engine7 7.1.105 → 7.1.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -530,6 +530,10 @@ function formatForLlm(i) {
530
530
  function formatLog(i) {
531
531
  return toWall(i ?? /* @__PURE__ */ new Date());
532
532
  }
533
+ function formatShort(i) {
534
+ const p = wallParts(i ?? /* @__PURE__ */ new Date());
535
+ return `${p.mo}/${p.d} ${p2(p.hh)}:${p2(p.mm)}`;
536
+ }
533
537
  function warnOnce(callSite, shape, sample) {
534
538
  const key = `${callSite}:${shape}`;
535
539
  if (warned.has(key)) return;
@@ -19965,17 +19969,46 @@ var init_filters = __esm({
19965
19969
  });
19966
19970
 
19967
19971
  // src/mem7/core/vector-store.ts
19968
- import * as lancedb from "@lancedb/lancedb";
19969
19972
  import { Schema, Field, Utf8, Float32, Int64, FixedSizeList, TimestampMicrosecond, List } from "apache-arrow";
19970
19973
  import { mkdirSync as mkdirSync14 } from "node:fs";
19971
19974
  import { join as join32 } from "node:path";
19972
- var VECTOR_DIM, BM25_FIELDS, RRF_K, warnedFts, vecField, commonFields, SCHEMAS, Mem7VectorStore;
19975
+ function lanceSpecifierFor(platform2, arch) {
19976
+ return platform2 === "darwin" && arch === "x64" ? "@engine7/lancedb-intel" : "@lancedb/lancedb";
19977
+ }
19978
+ function loadLance() {
19979
+ if (!lancePromise) {
19980
+ const spec = lanceSpecifierFor(process.platform, process.arch);
19981
+ lancePromise = import(spec).then(
19982
+ // 转发包是 CJS(module.exports = require(...)),命名导出可能落在 default 上
19983
+ (m) => m?.connect ? m : m?.default,
19984
+ (err) => {
19985
+ mem7VectorWarning = err?.message ? String(err.message) : String(err);
19986
+ console.warn(
19987
+ `[mem7] \u5411\u91CF\u7D22\u5F15\u4E0D\u53EF\u7528\uFF1A\u52A0\u8F7D ${spec} \u5931\u8D25 \u2014\u2014 ${mem7VectorWarning}
19988
+ mem7 \u964D\u7EA7\u505C\u7528\uFF0C\u5F15\u64CE\u5176\u4F59\u529F\u80FD\u4E0D\u53D7\u5F71\u54CD\u3002` + (isIntelMac ? "\n Intel Mac \u63D0\u793A\uFF1Ae7 \u4F9D\u8D56 @engine7/lancedb-intel\uFF08lancedb \u4E0D\u518D\u53D1\u5E03 darwin-x64 \u4E8C\u8FDB\u5236\uFF09\u3002\u82E5\u62A5\u300CCannot find native binding\u300D\uFF0C\u5220\u6389 node_modules \u91CD\u88C5\u4E00\u6B21\u5373\u53EF\u3002" : "")
19989
+ );
19990
+ return null;
19991
+ }
19992
+ );
19993
+ }
19994
+ return lancePromise;
19995
+ }
19996
+ var isIntelMac, lancePromise, mem7VectorWarning, Mem7VectorUnavailableError, VECTOR_DIM, BM25_FIELDS, RRF_K, warnedFts, vecField, commonFields, SCHEMAS, Mem7VectorStore;
19973
19997
  var init_vector_store = __esm({
19974
19998
  "src/mem7/core/vector-store.ts"() {
19975
19999
  "use strict";
19976
20000
  init_store2();
19977
20001
  init_tokenizer();
19978
20002
  init_filters();
20003
+ isIntelMac = process.platform === "darwin" && process.arch === "x64";
20004
+ lancePromise = null;
20005
+ mem7VectorWarning = null;
20006
+ Mem7VectorUnavailableError = class extends Error {
20007
+ constructor() {
20008
+ super(`mem7 \u5411\u91CF\u7D22\u5F15\u4E0D\u53EF\u7528\uFF08lancedb \u672A\u80FD\u52A0\u8F7D\uFF09${mem7VectorWarning ? `\uFF1A${mem7VectorWarning}` : ""}`);
20009
+ this.name = "Mem7VectorUnavailableError";
20010
+ }
20011
+ };
19979
20012
  VECTOR_DIM = 1024;
19980
20013
  BM25_FIELDS = {
19981
20014
  foresight: ["foresight_tokens", "evidence_tokens"],
@@ -20038,8 +20071,10 @@ var init_vector_store = __esm({
20038
20071
  }
20039
20072
  async conn() {
20040
20073
  if (!this.db) {
20074
+ const lance = await loadLance();
20075
+ if (!lance) throw new Mem7VectorUnavailableError();
20041
20076
  mkdirSync14(this.dir, { recursive: true });
20042
- this.db = await lancedb.connect(this.dir);
20077
+ this.db = await lance.connect(this.dir);
20043
20078
  }
20044
20079
  return this.db;
20045
20080
  }
@@ -20061,10 +20096,12 @@ var init_vector_store = __esm({
20061
20096
  * 幂等:已建则跳过(索引缺失才建)。
20062
20097
  */
20063
20098
  async ensureFtsIndexes(kind, tbl) {
20099
+ const lance = await loadLance();
20100
+ if (!lance) return;
20064
20101
  for (const col of BM25_FIELDS[kind]) {
20065
20102
  try {
20066
20103
  await tbl.createIndex(col, {
20067
- config: lancedb.Index.fts({
20104
+ config: lance.Index.fts({
20068
20105
  baseTokenizer: "whitespace",
20069
20106
  removeStopWords: false,
20070
20107
  lowercase: true,
@@ -20537,7 +20574,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
20537
20574
  blocks.push(block);
20538
20575
  count++;
20539
20576
  }
20540
- head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${(/* @__PURE__ */ new Date()).toISOString()}'`);
20577
+ head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${formatMdTimestamp()}'`);
20541
20578
  writeFileSync17(file2, head + "\n\n" + blocks.join("\n\n") + "\n", "utf8");
20542
20579
  }
20543
20580
  /**
@@ -20957,7 +20994,7 @@ function calcDurationDays(startDate, endDate) {
20957
20994
  const ms = Date.parse(endDate + "T00:00:00Z") - Date.parse(startDate + "T00:00:00Z");
20958
20995
  return Number.isNaN(ms) ? null : Math.round(ms / 864e5);
20959
20996
  }
20960
- function cellDateCst(cell) {
20997
+ function cellDateKey(cell) {
20961
20998
  return todayKey(cell.startTs);
20962
20999
  }
20963
21000
  async function extractForesights(cell, ownerId, provider, model) {
@@ -20986,7 +21023,7 @@ async function extractForesights(cell, ownerId, provider, model) {
20986
21023
  }
20987
21024
  }
20988
21025
  if (!raw) return [];
20989
- const startFallback = cellDateCst(cell);
21026
+ const startFallback = cellDateKey(cell);
20990
21027
  const out = [];
20991
21028
  for (const r of raw) {
20992
21029
  if (!r || typeof r !== "object") continue;
@@ -21027,7 +21064,7 @@ var init_foresight = __esm({
21027
21064
  });
21028
21065
 
21029
21066
  // src/mem7/core/pipeline.ts
21030
- function dateCst(tsMs) {
21067
+ function dateKey(tsMs) {
21031
21068
  return todayKey(tsMs);
21032
21069
  }
21033
21070
  async function runMem7Pipeline(args) {
@@ -21062,7 +21099,7 @@ async function runMem7Pipeline(args) {
21062
21099
  store.saveTail(sessionId, tail);
21063
21100
  for (const cell of cells) {
21064
21101
  const userSenders = [...new Set(cell.messages.filter((m) => m.role === "user").map((m) => m.senderId))];
21065
- const dateStr = dateCst(cell.startTs);
21102
+ const dateStr = dateKey(cell.startTs);
21066
21103
  const cellTimeIso = new Date(cell.startTs).toISOString();
21067
21104
  let episodeOut = null;
21068
21105
  let factsOut = [];
@@ -21235,11 +21272,18 @@ function parseProactiveConfig(raw) {
21235
21272
  console.warn(`[proactive] model "${modelRef}" \u975E\u6CD5\uFF08${err.message}\uFF09\u2014\u2014 \u6539\u7528\u4E3B\u6A21\u578B`);
21236
21273
  }
21237
21274
  }
21275
+ const num = (v, dflt) => {
21276
+ const n = typeof v === "number" ? v : Number(v);
21277
+ return Number.isFinite(n) && n >= 0 ? n : dflt;
21278
+ };
21238
21279
  return {
21239
21280
  enabled: obj.enabled === true || obj.enabled === "yes" || obj.enabled === 1,
21240
21281
  modelRef: parsed ? `${parsed.providerId}/${parsed.modelId}` : "",
21241
21282
  providerId: parsed?.providerId ?? "",
21242
- modelId: parsed?.modelId ?? ""
21283
+ modelId: parsed?.modelId ?? "",
21284
+ // 默认 3 天 = 翀哥的"以周为单位";staleHours 默认 36 = 与旧行为一致
21285
+ leadGateDays: num(obj.leadGateDays, 3),
21286
+ staleHours: num(obj.staleHours, 36)
21243
21287
  };
21244
21288
  }
21245
21289
  function proactiveDbPath(workspace) {
@@ -21603,6 +21647,8 @@ function rowToAction(r) {
21603
21647
  createdAt: r.created_at,
21604
21648
  completedAt: r.completed_at ?? void 0,
21605
21649
  cancelReason: r.cancel_reason ?? void 0,
21650
+ injectedAt: r.injected_at ?? void 0,
21651
+ note: r.note ?? void 0,
21606
21652
  provenance: r.provenance ? JSON.parse(r.provenance) : void 0
21607
21653
  };
21608
21654
  }
@@ -21652,6 +21698,8 @@ CREATE TABLE IF NOT EXISTS actions (
21652
21698
  created_at TEXT NOT NULL,
21653
21699
  completed_at TEXT,
21654
21700
  cancel_reason TEXT,
21701
+ injected_at TEXT,
21702
+ note TEXT,
21655
21703
  provenance TEXT
21656
21704
  );
21657
21705
  CREATE INDEX IF NOT EXISTS idx_actions_due ON actions(status, scheduled_time);
@@ -21691,6 +21739,8 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
21691
21739
  this.db.exec(SCHEMA10);
21692
21740
  const actionCols = new Set(this.db.prepare("PRAGMA table_info(actions)").all().map((r) => r.name));
21693
21741
  if (!actionCols.has("provenance")) this.db.exec("ALTER TABLE actions ADD COLUMN provenance TEXT");
21742
+ if (!actionCols.has("injected_at")) this.db.exec("ALTER TABLE actions ADD COLUMN injected_at TEXT");
21743
+ if (!actionCols.has("note")) this.db.exec("ALTER TABLE actions ADD COLUMN note TEXT");
21694
21744
  const decisionCols = new Set(this.db.prepare("PRAGMA table_info(proactive_decisions)").all().map((r) => r.name));
21695
21745
  if (!decisionCols.has("coverage_matched")) this.db.exec("ALTER TABLE proactive_decisions ADD COLUMN coverage_matched INTEGER NOT NULL DEFAULT 0");
21696
21746
  this.migrateNakedLocalTimes();
@@ -21954,16 +22004,51 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
21954
22004
  ).run(reason, nowIso2, ...intentIds);
21955
22005
  return Number(res.changes);
21956
22006
  }
22007
+ /**
22008
+ * 改状态。**只更新显式传入的列**——此前是「任一字段有值就把两列都写」,
22009
+ * 未传的那列被写成 NULL:sweep 的 R2 复检只传 cancelReason,会把已存在的
22010
+ * completed_at 清空(0925 修)。
22011
+ */
21957
22012
  updateActionStatus(id, status, extra) {
21958
- if (extra?.completedAt !== void 0 || extra?.cancelReason !== void 0) {
21959
- this.db.prepare("UPDATE actions SET status=?, completed_at=?, cancel_reason=? WHERE id=?").run(status, extra?.completedAt ?? null, extra?.cancelReason ?? null, id);
21960
- } else {
21961
- this.db.prepare("UPDATE actions SET status=? WHERE id=?").run(status, id);
22013
+ const sets = ["status=?"];
22014
+ const vals = [status];
22015
+ if (extra?.completedAt !== void 0) {
22016
+ sets.push("completed_at=?");
22017
+ vals.push(extra.completedAt);
22018
+ }
22019
+ if (extra?.cancelReason !== void 0) {
22020
+ sets.push("cancel_reason=?");
22021
+ vals.push(extra.cancelReason);
22022
+ }
22023
+ if (extra?.note !== void 0) {
22024
+ sets.push("note=?");
22025
+ vals.push(extra.note);
21962
22026
  }
22027
+ vals.push(id);
22028
+ this.db.prepare(`UPDATE actions SET ${sets.join(", ")} WHERE id=?`).run(...vals);
22029
+ }
22030
+ /** sweep 注入前标记(防重复注入;复刻 due→推送的原子语义)。
22031
+ * 0925:同时记 injected_at —— 收割机判"超时"必须拿**注入时刻**,不能拿排期/创建时刻。 */
22032
+ markActionReminded(id, injectedAt) {
22033
+ this.db.prepare(`UPDATE actions SET status='executing', injected_at=? WHERE id=? AND status='queued'`).run(injectedAt, id);
22034
+ }
22035
+ /**
22036
+ * 注入失败回滚(0925):submitMessage 被 dispatcher guard 拒了、或主 session 不在——
22037
+ * 此时 action 已被标 executing 却从没进过 agent 的视野,就是一条"谁也没见过"的僵尸。
22038
+ * 退回去重排,下一轮 sweep 还能再试。返回回滚条数。
22039
+ */
22040
+ rollbackReminded(ids) {
22041
+ if (ids.length === 0) return 0;
22042
+ const ph = ids.map(() => "?").join(",");
22043
+ const res = this.db.prepare(
22044
+ `UPDATE actions SET status='queued', injected_at=NULL WHERE status='executing' AND id IN (${ph})`
22045
+ ).run(...ids);
22046
+ return Number(res.changes);
21963
22047
  }
21964
- /** sweep 注入前标记(防重复注入;复刻 due→推送的原子语义) */
21965
- markActionReminded(id) {
21966
- this.db.prepare(`UPDATE actions SET status='executing' WHERE id=? AND status='queued'`).run(id);
22048
+ /** 某状态的条数(对账告警用:executing 只增不减 = 闭环断了) */
22049
+ countByStatus(status) {
22050
+ const r = this.db.prepare("SELECT COUNT(*) AS c FROM actions WHERE status=?").get(status);
22051
+ return Number(r?.c ?? 0);
21967
22052
  }
21968
22053
  /**
21969
22054
  * 0915:executing 超时收割。sweep 注入时标 executing,agent 处理后标终态;
@@ -21972,10 +22057,12 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
21972
22057
  * scheduled_time 或 created_at 距今超 staleHours 的 executing → cancelled。返回收割条数。
21973
22058
  */
21974
22059
  reapStaleExecuting(nowIso2, staleHours = 36) {
22060
+ if (!(staleHours > 0)) return 0;
21975
22061
  const cutoff = new Date(Date.parse(nowIso2) - staleHours * 36e5).toISOString();
21976
22062
  const res = this.db.prepare(
21977
- `UPDATE actions SET status='cancelled', completed_at=?, cancel_reason=? WHERE status='executing' AND (scheduled_time < ? OR created_at < ?)`
21978
- ).run(nowIso2, `executing \u8D85\u65F6\u6536\u5272\uFF08\u6CE8\u5165\u540E ${staleHours}h \u672A\u95ED\u73AF\uFF09`, cutoff, cutoff);
22063
+ `UPDATE actions SET status='cancelled', completed_at=?, cancel_reason=?
22064
+ WHERE status='executing' AND COALESCE(injected_at, created_at) < ?`
22065
+ ).run(nowIso2, `\u672A\u6536\u5230\u56DE\u6267\uFF08\u6CE8\u5165\u540E ${staleHours}h \u65E0\u95ED\u73AF\u4FE1\u53F7\uFF1B\u539F\u56E0\u672A\u77E5\uFF09`, cutoff);
21979
22066
  return Number(res.changes);
21980
22067
  }
21981
22068
  /**
@@ -22545,12 +22632,17 @@ ${D.intent_node_example}
22545
22632
  \`\`\`
22546
22633
 
22547
22634
  **Intent fields:**
22548
- - \`suggested_time\`: When to surface/execute this intent
22549
- - \`expiry\`: When this intent is no longer relevant
22635
+ - \`suggested_time\`: When to surface/execute this intent\uFF08\u89C1\u4E0B\u65B9\u65F6\u95F4\u683C\u5F0F\uFF09
22636
+ - \`expiry\`: When this intent is no longer relevant\uFF08\u89C1\u4E0B\u65B9\u65F6\u95F4\u683C\u5F0F\uFF09
22550
22637
  - \`priority\`: "low", "medium", "high", or "urgent"
22551
22638
  - \`status\`: "pending", "action_scheduled", or "resolved"
22552
22639
  - \`related_time_node\`: Link to a TIME node for urgency tracking
22553
- - \`pattern_source\`: The concept ID that triggered this intent (for pattern-based intents)`;
22640
+ - \`pattern_source\`: The concept ID that triggered this intent (for pattern-based intents)
22641
+
22642
+ **Time field format (applies to every *_time field below):**
22643
+ - Write **local wall time with NO timezone suffix** \u2014 \`"2026-09-21T09:00:00"\` means 9am in ${tz()}. This is the only accepted form.
22644
+ - **Never append \`Z\` or \`+08:00\`.** A suffixed value is read as an absolute instant, so \`"2026-09-21T09:00:00Z"\` lands at 5pm ${tz()}, not 9am \u2014 and it is logged as a contract violation.
22645
+ - Anchor on the \`## Current Time\` line when there is one, and convert relative phrasing ("\u660E\u65E99\u70B9") into that local clock.`;
22554
22646
  const SECTION_INTENTS_PATTERNS = `
22555
22647
  ## Pattern-Based Intents
22556
22648
 
@@ -22937,7 +23029,7 @@ ${contextText}
22937
23029
 
22938
23030
  ## Current Time
22939
23031
 
22940
- ${args.now.toISOString()}
23032
+ ${formatForLlm(args.now)} \u2190 use THIS as "now" (local wall time, NOT UTC)
22941
23033
 
22942
23034
  ## Existing Commitments (calendar / current work state)
22943
23035
 
@@ -23044,6 +23136,7 @@ var DEFAULT_DOMAIN, registryCache, DEFAULT_SECTION_ORDER, MODE_PROMPTS2, OPPORTU
23044
23136
  var init_prompts = __esm({
23045
23137
  "src/memory/proactive/prompts.ts"() {
23046
23138
  "use strict";
23139
+ init_clock();
23047
23140
  DEFAULT_DOMAIN = {
23048
23141
  domain_description: "\u4F60\u4E0E TA \u5171\u540C\u7684\u751F\u6D3B\u2014\u2014TA \u7684\u5DE5\u4F5C\u4E0E\u9879\u76EE\u3001\u5BB6\u4EBA\u670B\u53CB\u7684\u5F80\u6765\u3001\u5065\u5EB7\u3001\u5B66\u4E60\u6210\u957F\u548C\u65E5\u5E38\u4E8B\u52A1",
23049
23142
  event_type_desc: "\u5BF9\u8BDD\u4E2D\u53D1\u751F\u7684\u4E00\u4EF6\u4E8B\u2014\u2014TA \u8BF4\u7684\u8BDD\u3001\u4E0E\u4EBA\u7684\u4E92\u52A8\u3001\u63D0\u5230\u7684\u884C\u7A0B\u6216\u8BA1\u5212",
@@ -23065,12 +23158,12 @@ var init_prompts = __esm({
23065
23158
  time_node_example: JSON.stringify({
23066
23159
  op: "ADD_NODE",
23067
23160
  node_type: "time",
23068
- data: { id: "t-example", title: "\u4F5C\u54C1\u63D0\u4EA4\u622A\u6B62", scheduled_time: "2026-09-21T23:59:00Z", recurrence: null, urgency_window_hours: 24 }
23161
+ data: { id: "t-example", title: "\u4F5C\u54C1\u63D0\u4EA4\u622A\u6B62", scheduled_time: "2026-09-21T23:59:00", recurrence: null, urgency_window_hours: 24 }
23069
23162
  }, null, 2),
23070
23163
  intent_node_example: JSON.stringify({
23071
23164
  op: "ADD_NODE",
23072
23165
  node_type: "intent",
23073
- data: { intent_id: "i-example", title: "\u63D0\u9192 TA \u786E\u8BA4\u4F5C\u54C1\u63D0\u4EA4\u72B6\u6001", description: "\u63D0\u4EA4 9/21 \u622A\u6B62\uFF0C\u63D0\u524D\u786E\u8BA4\u72B6\u6001", status: "pending", priority: "high", suggested_time: "2026-09-18T09:00:00Z", expiry: "2026-09-21T23:59:00Z" }
23166
+ data: { intent_id: "i-example", title: "\u63D0\u9192 TA \u786E\u8BA4\u4F5C\u54C1\u63D0\u4EA4\u72B6\u6001", description: "\u63D0\u4EA4 9/21 \u622A\u6B62\uFF0C\u63D0\u524D\u786E\u8BA4\u72B6\u6001", status: "pending", priority: "high", suggested_time: "2026-09-18T09:00:00", expiry: "2026-09-21T23:59:00" }
23074
23167
  }, null, 2),
23075
23168
  pattern_intent_examples: [
23076
23169
  "**\u6A21\u5F0F**\uFF1ATA \u6BCF\u5468\u4E09\u665A\u6709\u56FA\u5B9A\u5B89\u6392\uFF08\u6982\u5FF5 c-ta-schedule\uFF0Cstrength 0.7\uFF09",
@@ -23203,7 +23296,7 @@ Return valid JSON with this structure:
23203
23296
  {
23204
23297
  "title": "Short action title (max 50 chars)",
23205
23298
  "description": "Detailed description of what to do",
23206
- "scheduled_time": "ISO 8601 datetime when to execute",
23299
+ "scheduled_time": "Local wall time, NO timezone suffix - '2026-09-21T09:00:00' means 9am ${tz()}",
23207
23300
  "estimated_duration_minutes": 30,
23208
23301
  "urgency": "low|medium|high|urgent"
23209
23302
  }
@@ -23279,7 +23372,7 @@ If deadline has no time context, fall back to normal timing rules.
23279
23372
  });
23280
23373
 
23281
23374
  // src/memory/proactive/calendar-bridge.ts
23282
- function toBeijingParts(iso) {
23375
+ function toCalendarWallParts(iso) {
23283
23376
  const b = wallParts(iso);
23284
23377
  const dateStr = `${b.mo}/${b.d}`;
23285
23378
  const timeExact = `${String(b.hh).padStart(2, "0")}:${String(b.mm).padStart(2, "0")}`;
@@ -23292,13 +23385,19 @@ function parseCalendarDateMs(dateStr, now) {
23292
23385
  const day = parseInt(parts[1], 10);
23293
23386
  if (!Number.isFinite(month) || !Number.isFinite(day)) return null;
23294
23387
  let year = wallParts(now).y;
23295
- let candidate = new Date(year, month - 1, day).getTime();
23388
+ let candidate = wallMidnightMs(year, month, day);
23389
+ if (candidate === null) return null;
23296
23390
  if (candidate < now.getTime() - 7 * 864e5) {
23297
23391
  year += 1;
23298
- candidate = new Date(year, month - 1, day).getTime();
23392
+ candidate = wallMidnightMs(year, month, day);
23299
23393
  }
23300
23394
  return candidate;
23301
23395
  }
23396
+ function wallMidnightMs(year, month, day) {
23397
+ const p22 = (n) => String(n).padStart(2, "0");
23398
+ const ms = Date.parse(parseWall(`${year}-${p22(month)}-${p22(day)}T00:00:00`));
23399
+ return Number.isNaN(ms) ? null : ms;
23400
+ }
23302
23401
  function injectCalendarEvents(store, calDb, days = 30, now = /* @__PURE__ */ new Date()) {
23303
23402
  const cutoffMs = now.getTime() + days * 864e5;
23304
23403
  const rows = listEventsForInjection(calDb, 50);
@@ -23505,6 +23604,16 @@ function enqueueOpportunity(persist, opp, anchor, planningContext, previousTitle
23505
23604
  console.log(`[proactive] opportunity enqueued (${anchor.label}): ${opp.title}`);
23506
23605
  return true;
23507
23606
  }
23607
+ function isProfilePrediction(text) {
23608
+ return PROFILE_RE.test(text);
23609
+ }
23610
+ function capForesightEntries(entries2, today, openCap = 12, upcomingCap = 8) {
23611
+ const isOpen = (e) => e.start <= today;
23612
+ const sorted = entries2.slice().sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || (isOpen(a) ? a.end.localeCompare(b.end) : a.start.localeCompare(b.start)));
23613
+ const open = sorted.filter(isOpen).slice(0, openCap);
23614
+ const upcoming = sorted.filter((e) => !isOpen(e)).slice(0, upcomingCap);
23615
+ return [...open, ...upcoming];
23616
+ }
23508
23617
  function foresightMatchKey(text) {
23509
23618
  return text.replace(/\n+/g, " ").slice(0, FORESIGHT_TEXT_MAX).replace(/\s+/g, "");
23510
23619
  }
@@ -23554,7 +23663,16 @@ function parseForesightEntries(everosRoot, now, userId) {
23554
23663
  const key = text.replace(/\s+/g, "");
23555
23664
  if (seen.has(key)) continue;
23556
23665
  seen.add(key);
23557
- out.push({ start, end, text: text.replace(/\n+/g, " ").slice(0, FORESIGHT_TEXT_MAX), evidence: evidence.replace(/\n+/g, " ").slice(0, 80), ownerId: blockOwner });
23666
+ const textFlat = text.replace(/\n+/g, " ");
23667
+ out.push({
23668
+ start,
23669
+ end,
23670
+ text: textFlat.slice(0, FORESIGHT_TEXT_MAX),
23671
+ evidence: evidence.replace(/\n+/g, " ").slice(0, 80),
23672
+ ownerId: blockOwner,
23673
+ // 画像型在这里就标出来,扫描路径据此剔除(见 isProfilePrediction 的注释)
23674
+ kind: isProfilePrediction(textFlat) ? "profile" : "event"
23675
+ });
23558
23676
  }
23559
23677
  }
23560
23678
  return out;
@@ -23608,7 +23726,10 @@ async function runOpportunityScan(deps) {
23608
23726
  return ms !== null && ms >= now.getTime() - 864e5 && ms <= now.getTime() + 31 * 864e5;
23609
23727
  }).sort((a, b) => (parseCalendarDateMs(a.date, now) ?? 0) - (parseCalendarDateMs(b.date, now) ?? 0)).slice(0, 30).map((c) => `- [calendar #${c.id}] ${c.date} ${c.time} ${c.title}`);
23610
23728
  const userPrompt = [
23611
- `\u5F53\u524D\u65F6\u95F4\uFF1A${now.toISOString()}`,
23729
+ // 这一行必须与上面的 calendar 行同坐标:calendar 的 date/time 是**配置时区**墙上时间,
23730
+ // 而这里此前给的是 UTC Z —— 同一个 prompt 两套坐标、且都没标注,模型无从判断"现在"。
23731
+ `\u5F53\u524D\u65F6\u95F4\uFF1A${formatForLlm(now)}`,
23732
+ `\uFF08\u4E0B\u9762 calendar \u884C\u7684 date/time \u4E5F\u662F ${tz()} \u672C\u5730\u65F6\u523B\uFF0C\u4E0E\u4E4B\u540C\u4E00\u5750\u6807\uFF09`,
23612
23733
  "",
23613
23734
  "## \u6302\u7740\u7684 intent\uFF08\u672A\u5B8C\u7ED3\uFF09",
23614
23735
  activeIntents.length > 0 ? activeIntents.join("\n") : "\uFF08\u65E0\uFF09",
@@ -23616,7 +23737,8 @@ async function runOpportunityScan(deps) {
23616
23737
  "## \u672A\u6765\u65E5\u7A0B\uFF08\u672A\u6765 30 \u5929 calendar\uFF09",
23617
23738
  calendarLines.length > 0 ? calendarLines.join("\n") : "\uFF08\u65E0\uFF09",
23618
23739
  "",
23619
- "## \u9884\u6D4B\uFF08everos foresight\u2014\u2014\u6B64\u524D\u5BF9\u8BDD\u4E2D LLM \u9884\u6D4B\u7684\u672A\u6765\u4E8B\u4EF6\uFF0C\u5E26\u65F6\u95F4\u7A97\u53E3\uFF1B\u662F\u9884\u6D4B\u4E0D\u662F\u4E8B\u5B9E\uFF09",
23740
+ // 0919 删轨 B 后源头已是 mem7(945be93c 漏改的措辞)
23741
+ "## \u9884\u6D4B\uFF08mem7 foresight\u2014\u2014\u6B64\u524D\u5BF9\u8BDD\u4E2D LLM \u9884\u6D4B\u7684\u672A\u6765\u4E8B\u4EF6\uFF0C\u5E26\u65F6\u95F4\u7A97\u53E3\uFF1B\u662F\u9884\u6D4B\u4E0D\u662F\u4E8B\u5B9E\uFF09",
23620
23742
  (deps.foresightLines ?? []).length > 0 ? (deps.foresightLines ?? []).join("\n") : "\uFF08\u65E0\uFF09",
23621
23743
  "",
23622
23744
  "## \u8FD1\u671F\u4E8B\u4EF6\uFF08\u6700\u65B0 15 \u6761\uFF09",
@@ -23696,7 +23818,7 @@ async function runOpportunityScan(deps) {
23696
23818
  }
23697
23819
  return enqueued;
23698
23820
  }
23699
- var OPPORTUNITY_SCAN_INTERVAL_MS, foresightCache, FORESIGHT_CACHE_TTL_MS, FORESIGHT_TEXT_MAX, FS_MATCH_STOP;
23821
+ var OPPORTUNITY_SCAN_INTERVAL_MS, PROFILE_RE, foresightCache, FORESIGHT_CACHE_TTL_MS, FORESIGHT_TEXT_MAX, FS_MATCH_STOP;
23700
23822
  var init_opportunity = __esm({
23701
23823
  "src/memory/proactive/opportunity.ts"() {
23702
23824
  "use strict";
@@ -23706,6 +23828,7 @@ var init_opportunity = __esm({
23706
23828
  init_recall_score();
23707
23829
  init_clock();
23708
23830
  OPPORTUNITY_SCAN_INTERVAL_MS = 6 * 60 * 6e4;
23831
+ PROFILE_RE = /今后|以后|将来|往后|再次遇到|下次遇到|会一直|会习惯|会继续|会优先|会避免|会主动|会定期|会倾向于|会按|会先|时会|倾向于|更愿意|养成了?.*习惯|将养成|将优先|将避免|习惯性地|will always|will prefer|will avoid|will continue|will keep|will strictly|tends? to|is likely to (?:always|keep|continue)/i;
23709
23832
  foresightCache = /* @__PURE__ */ new Map();
23710
23833
  FORESIGHT_CACHE_TTL_MS = 30 * 6e4;
23711
23834
  FORESIGHT_TEXT_MAX = 150;
@@ -24714,48 +24837,91 @@ var init_intent = __esm({
24714
24837
 
24715
24838
  // src/memory/proactive/sweep.ts
24716
24839
  import { readFileSync as readFileSync32 } from "node:fs";
24840
+ function sharesTerm(a, b) {
24841
+ const x = normalizeTitle(a);
24842
+ const y = normalizeTitle(b);
24843
+ if (x.length < 2 || y.length < 2) return false;
24844
+ const grams = new Set(Array.from({ length: x.length - 1 }, (_, i) => x.slice(i, i + 2)));
24845
+ for (let i = 0; i < y.length - 1; i++) if (grams.has(y.slice(i, i + 2))) return true;
24846
+ return false;
24847
+ }
24848
+ function isSameCommitment(a, b) {
24849
+ const x = normalizeTitle(a);
24850
+ const y = normalizeTitle(b);
24851
+ if (!x || !y) return false;
24852
+ if (x === y) return true;
24853
+ if (x.length >= 4 && y.includes(x)) return true;
24854
+ if (y.length >= 4 && x.includes(y)) return true;
24855
+ return false;
24856
+ }
24717
24857
  function actionPayload(a, now, recentEvidence) {
24718
24858
  let suggestedTask = null;
24859
+ let scheduledLocal = null;
24719
24860
  try {
24720
- const { dateStr, timeExact } = toBeijingParts(a.scheduledTime);
24861
+ const { dateStr, timeExact } = toCalendarWallParts(a.scheduledTime);
24721
24862
  suggestedTask = { date: dateStr, time: timeExact };
24863
+ scheduledLocal = formatForLlm(a.scheduledTime);
24864
+ } catch {
24865
+ }
24866
+ let generatedLocal = null;
24867
+ try {
24868
+ generatedLocal = a.createdAt ? formatForLlm(a.createdAt) : null;
24722
24869
  } catch {
24723
24870
  }
24724
24871
  const ageHours = (now.getTime() - Date.parse(a.scheduledTime)) / 36e5;
24725
- const staleHint = ageHours > 36 ? "\u53EF\u80FD\u8FC7\u65F6\uFF0C\u6CE8\u610F\u6838\u5BF9\u2014\u2014\u751F\u6210\u4E8E\u5F88\u4E45\u4E4B\u524D\uFF0C\u4E8B\u6001\u53EF\u80FD\u5DF2\u53D8\u5316" : void 0;
24872
+ const staleHint = ageHours > STALE_HINT_HOURS ? `\u8FD9\u6761\u5DF2\u8FC7\u6392\u671F ${Math.round(ageHours)} \u5C0F\u65F6\u2014\u2014\u6B63\u6587\u91CC\u7684"\u73B0\u5728/\u4ECA\u5929"\u6307\u7684\u662F**\u751F\u6210\u65F6\u523B**\uFF0C\u4E0D\u662F\u6B64\u523B\uFF0C\u5148\u6838\u5BF9\u518D\u5224` : void 0;
24726
24873
  return {
24727
24874
  action_id: a.id,
24728
24875
  intent_id: a.intentId,
24729
24876
  title: a.title,
24730
24877
  description: a.description,
24731
24878
  scheduled_time: a.scheduledTime,
24879
+ // UTC 绝对时刻(机器坐标)
24880
+ scheduled_time_local: scheduledLocal,
24881
+ // 本地可读 —— 与上一行是同一个时刻,别当两件事
24882
+ suggested_task: suggestedTask,
24883
+ // { date, time } 本地墙上时间,calendar add-task 参数
24732
24884
  urgency: a.urgency,
24733
24885
  duration_minutes: a.durationMinutes,
24734
24886
  calendar_id: a.calendarId,
24735
24887
  status: a.status,
24736
- suggested_task: suggestedTask,
24888
+ generated_at_local: generatedLocal,
24889
+ // 这条 action 是**几点生成的**(正文里的「现在」指这个点)
24737
24890
  stale_hint: staleHint,
24738
- // 0906 anti-hindsight:近期关联证据(最新在前)——"后续对话已做完"的马后炮当场现形
24739
- recent_evidence: recentEvidence && recentEvidence.length > 0 ? recentEvidence.map((e) => `[${e.createdAt.slice(5, 16).replace("T", " ")}] ${e.title}${e.description ? `\uFF1A${e.description}` : ""}`) : void 0,
24891
+ // 0906 anti-hindsight:近期关联证据(最新在前)——"后续对话已做完"的马后炮当场现形。
24892
+ // 时间戳走 formatShort(本地);此前是 createdAt.slice(5,16) —— 那是 **UTC**,
24893
+ // 混在 suggested_task(本地)旁边且无标注。
24894
+ recent_evidence: recentEvidence && recentEvidence.length > 0 ? recentEvidence.map((e) => `[${formatShort(e.createdAt)}] ${e.title}${e.description ? `\uFF1A${e.description}` : ""}`) : void 0,
24740
24895
  // 机械可判的过期事实标记(不靠 LLM):action 创建之后 graph 又来了新证据 →
24741
24896
  // action 基于过期事实生成,先核对新证据再决定(生成侧 Time-Evidence Gate 的兜底)
24742
24897
  stale_facts: recentEvidence && recentEvidence.length > 0 && recentEvidence.some((e) => Date.parse(e.createdAt) > Date.parse(a.createdAt)) ? "\u6B64 action \u751F\u6210\u540E\u53C8\u6709\u65B0\u8BC1\u636E\uFF08\u89C1 recent_evidence \u6700\u65B0\u4E00\u6761\uFF09\u2014\u2014\u5148\u6838\u5BF9\u518D\u51B3\u5B9A\uFF0C\u591A\u534A\u8BE5 cancelled" : void 0,
24743
24898
  _received_at: now.toISOString()
24744
24899
  };
24745
24900
  }
24746
- var DEFAULT_PROMPT, Sweep;
24901
+ var BACKLOG_ALERT_RUNS, STALE_HINT_HOURS, DEFAULT_PROMPT, Sweep;
24747
24902
  var init_sweep = __esm({
24748
24903
  "src/memory/proactive/sweep.ts"() {
24749
24904
  "use strict";
24750
24905
  init_calendar_bridge();
24906
+ init_intent();
24907
+ init_clock();
24908
+ BACKLOG_ALERT_RUNS = 12;
24909
+ STALE_HINT_HOURS = 8;
24751
24910
  DEFAULT_PROMPT = `[proactive] \u6709 action \u5230\u671F\u4E86\u3002\u5904\u7406\u89C4\u5219\uFF1A
24752
24911
 
24753
- 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
24754
- 2. **status \u53EA\u6709**\uFF1Acompleted\uFF08\u5DF2\u5904\u7406\uFF0C\u542B\u5DF2\u544A\u8BC9 TA\uFF09/ cancelled\uFF08\u8DF3\u8FC7\uFF1A\u6280\u672F\u7C7B/\u5DF2\u8FC7\u65F6/\u91CD\u590D\uFF09/ failed\uFF08\u6267\u884C\u5931\u8D25\uFF09
24912
+ 1. **\u5FC5\u987B\u8C03\u5DE5\u5177\u843D\u72B6\u6001**\uFF08\u786C\u8981\u6C42\uFF0C\u4E0D\u662F\u5EFA\u8BAE\uFF09\uFF1A
24913
+ - \u5904\u7406\u5B8C \u2192 \`proactive_done\`\uFF08action_id\uFF0C\u843D\u8FDB\u65E5\u5386\u65F6\u5E26 calendar_id\uFF09
24914
+ - \u8DF3\u8FC7 \u2192 \`proactive_cancel\`\uFF08action_id + reason\uFF09
24915
+ \u26A0\uFE0F **\u5728\u56DE\u590D\u6B63\u6587\u91CC\u5199 \`act-xxx: cancelled\` \u4E0D\u7B97\u6807\u8BB0** \u2014\u2014 \u90A3\u662F\u7559\u75D5\uFF0C\u4E0D\u843D\u5E93\u3002
24916
+ \u7CFB\u7EDF\u6709\u4E00\u9053\u515C\u5E95\u89E3\u6790\u4F1A\u635E\u6B63\u6587\uFF0C\u4F46\u5DE5\u5177\u8DEF\u5F84\u624D\u662F\u6B63\u8DEF\uFF08\u7406\u7531\u66F4\u6743\u5A01\u3001\u80FD\u56DE\u586B calendar\uFF09\u3002
24917
+ 2. **\u6279\u91CF\u5904\u7406**\uFF1A\u5148\u5168\u90E8\u5224\u65AD\u5B8C\uFF0C\u518D\u7EDF\u4E00\u8C03\u5DE5\u5177\uFF08\u4E0D\u8981\u6BCF\u6761\u8C03\u4E00\u6B21\uFF09
24918
+ 3. **status \u53EA\u6709**\uFF1Acompleted\uFF08\u5DF2\u5904\u7406\uFF0C\u542B\u5DF2\u544A\u8BC9 TA\uFF09/ cancelled\uFF08\u8DF3\u8FC7\uFF1A\u6280\u672F\u7C7B/\u5DF2\u8FC7\u65F6/\u91CD\u590D\uFF09/ failed\uFF08\u6267\u884C\u5931\u8D25\uFF09
24755
24919
  \u26A0\uFE0F \u6CA1\u6709 notified\u3001\u6CA1\u6709 skipped
24756
- 3. **\u91CD\u590D\u68C0\u6D4B**\uFF1A\u591A\u6761 title \u9AD8\u5EA6\u76F8\u4F3C\u65F6\u53EA\u5904\u7406\u4E00\u6761\uFF0C\u5176\u4F59 cancelled
24757
- 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
24758
- 5. \u51B3\u5B9A\u544A\u8BC9 TA \u65F6\u7528\u4F60\u81EA\u5DF1\u7684\u8BED\u6C14\u2014\u2014\u8FD9\u662F\u4F60\u7684\u4E3B\u52A8\u5173\u6000\uFF0C\u4E0D\u662F\u7CFB\u7EDF\u901A\u77E5`;
24920
+ 4. **\u91CD\u590D\u68C0\u6D4B**\uFF1A\u591A\u6761 title \u9AD8\u5EA6\u76F8\u4F3C\u65F6\u53EA\u5904\u7406\u4E00\u6761\uFF0C\u5176\u4F59 cancelled
24921
+ 5. **\u5148\u6838\u5BF9\u518D\u5224**\uFF1A\u6B63\u6587\u91CC\u7684"\u73B0\u5728/\u4ECA\u5929"\u662F**\u751F\u6210\u65F6\u523B**\u7684\uFF0C\u4E0D\u662F\u6B64\u523B\u2014\u2014
24922
+ \u62FF \`generated_at_local\` \u8DDF \`_received_at\` \u5BF9\u4E00\u4E0B\uFF1B\u518D\u8DDF calendar/goal \u91CC\u5DF2\u6709\u7684\u5BF9\u4E00\u904D\u3002
24923
+ 6. **urgency \u662F\u53C2\u8003**\uFF08urgent=\u51E0\u5C0F\u65F6/high=24h/medium=48h/low=\u4E00\u5468\uFF09\uFF0C\u6309\u5B9E\u9645\u60C5\u51B5\u5224\u65AD
24924
+ 7. \u51B3\u5B9A\u544A\u8BC9 TA \u65F6\u7528\u4F60\u81EA\u5DF1\u7684\u8BED\u6C14\u2014\u2014\u8FD9\u662F\u4F60\u7684\u4E3B\u52A8\u5173\u6000\uFF0C\u4E0D\u662F\u7CFB\u7EDF\u901A\u77E5`;
24759
24925
  Sweep = class {
24760
24926
  constructor(deps, intervalMs = 3e5) {
24761
24927
  this.deps = deps;
@@ -24764,6 +24930,9 @@ var init_sweep = __esm({
24764
24930
  deps;
24765
24931
  intervalMs;
24766
24932
  timer = null;
24933
+ /** executing 连续增长的轮数(对账告警用) */
24934
+ backlogStreak = 0;
24935
+ lastExecuting = 0;
24767
24936
  start() {
24768
24937
  if (this.timer) return;
24769
24938
  this.timer = setInterval(() => {
@@ -24786,8 +24955,20 @@ var init_sweep = __esm({
24786
24955
  }
24787
24956
  /** 单轮:收割超时 executing → due → 标记 → 合并注入一次。返回注入条数 */
24788
24957
  runOnce(now) {
24789
- const reaped = this.deps.persist.reapStaleExecuting(now.toISOString());
24958
+ const reaped = this.deps.persist.reapStaleExecuting(now.toISOString(), this.deps.staleHours ?? 36);
24790
24959
  if (reaped > 0) console.log(`[proactive] reaped ${reaped} stale executing action(s)`);
24960
+ const execNow = this.deps.persist.countByStatus("executing");
24961
+ if (execNow > 0 && execNow >= this.lastExecuting) this.backlogStreak++;
24962
+ else this.backlogStreak = 0;
24963
+ this.lastExecuting = execNow;
24964
+ if (this.backlogStreak >= BACKLOG_ALERT_RUNS) {
24965
+ this.backlogStreak = 0;
24966
+ try {
24967
+ this.deps.onUnclosedBacklog?.(execNow);
24968
+ } catch (err) {
24969
+ console.warn(`[proactive] backlog alert failed: ${err.message}`);
24970
+ }
24971
+ }
24791
24972
  let due = this.deps.persist.dueActions(now.toISOString());
24792
24973
  if (due.length === 0) return 0;
24793
24974
  const resolvedActions = [];
@@ -24801,13 +24982,58 @@ var init_sweep = __esm({
24801
24982
  });
24802
24983
  if (resolvedActions.length > 0) {
24803
24984
  for (const { id, intentStatus } of resolvedActions) {
24804
- this.deps.persist.updateActionStatus(id, "cancelled", { cancelReason: `intent ${intentStatus}\u2014\u2014R2 sweep \u518D\u9A8C\u8BC1` });
24985
+ this.deps.persist.updateActionStatus(id, "cancelled", {
24986
+ completedAt: now.toISOString(),
24987
+ cancelReason: `intent ${intentStatus}\u2014\u2014R2 sweep \u518D\u9A8C\u8BC1`
24988
+ });
24805
24989
  }
24806
24990
  console.log(`[proactive] R2: ${resolvedActions.length} due action(s) cancelled (intent resolved)`);
24807
24991
  }
24992
+ const gatedItems = [];
24993
+ const gateOff = (a, reason) => {
24994
+ gatedItems.push({ id: a.id, title: a.title, reason });
24995
+ return false;
24996
+ };
24997
+ const coveredTitles = this.deps.coveredTitles ?? [];
24998
+ const coveredEvents = this.deps.coveredEvents ?? [];
24999
+ if (coveredTitles.length > 0 || coveredEvents.length > 0) {
25000
+ due = due.filter((a) => {
25001
+ if (coveredTitles.some((c) => isSameCommitment(a.title, c))) {
25002
+ return gateOff(a, "calendar/goal \u91CC\u5DF2\u7ECF\u6709\u540C\u4E00\u4EF6\u4E8B\u2014\u2014\u4E0D\u91CD\u590D\u63D0\u9192");
25003
+ }
25004
+ const parts = toCalendarWallParts(a.scheduledTime);
25005
+ const sameMoment = coveredEvents.some((ev) => ev.date === parts.dateStr && ev.time === parts.timeExact);
25006
+ if (sameMoment && coveredEvents.some((ev) => ev.date === parts.dateStr && ev.time === parts.timeExact && sharesTerm(a.title, ev.title))) {
25007
+ return gateOff(a, "\u540C\u4E00\u65F6\u523B calendar \u91CC\u5DF2\u6709\u540C\u4E00\u4EF6\u4E8B\uFF08\u65F6\u95F4\u951A\u70B9\u5224\u91CD\uFF09\u2014\u2014\u4E0D\u91CD\u590D\u63D0\u9192");
25008
+ }
25009
+ return true;
25010
+ });
25011
+ }
25012
+ const minLeadDays = this.deps.minLeadDays ?? 0;
25013
+ if (minLeadDays > 0) {
25014
+ due = due.filter((a) => {
25015
+ const leadMs = Date.parse(a.scheduledTime) - Date.parse(a.createdAt);
25016
+ if (leadMs >= minLeadDays * 864e5) return true;
25017
+ return gateOff(a, `\u63D0\u524D\u91CF\u4E0D\u8DB3 ${minLeadDays} \u5929\uFF08\u751F\u6210\u65F6\u79BB\u4E8B\u53D1\u592A\u8FD1\uFF09\u2014\u2014proactive \u53EA\u505A\u5468\u7EA7\u63D0\u524D\u91CF`);
25018
+ });
25019
+ }
25020
+ for (const g of gatedItems) {
25021
+ this.deps.persist.updateActionStatus(g.id, "cancelled", {
25022
+ completedAt: now.toISOString(),
25023
+ cancelReason: g.reason
25024
+ });
25025
+ }
25026
+ if (gatedItems.length > 0) {
25027
+ console.log(`[proactive] gated ${gatedItems.length} action(s): ${gatedItems.map((g) => g.reason.slice(0, 12)).join(" / ")}`);
25028
+ try {
25029
+ this.deps.onGated?.(gatedItems);
25030
+ } catch (err) {
25031
+ console.warn(`[proactive] onGated failed: ${err.message}`);
25032
+ }
25033
+ }
24808
25034
  if (due.length === 0) return 0;
24809
25035
  for (const act of due) {
24810
- this.deps.persist.markActionReminded(act.id);
25036
+ this.deps.persist.markActionReminded(act.id, now.toISOString());
24811
25037
  }
24812
25038
  const payload = due.map((a) => actionPayload(a, now, this.deps.persist.getIntentRecentEvents(a.intentId)));
24813
25039
  const blocks = [
@@ -24818,7 +25044,12 @@ ${JSON.stringify(payload, null, 2)}
24818
25044
  \`\`\`` },
24819
25045
  { type: "text", text: this.loadPrompt() }
24820
25046
  ];
24821
- this.deps.submit(blocks);
25047
+ const delivered = this.deps.submit(blocks);
25048
+ if (!delivered) {
25049
+ const back = this.deps.persist.rollbackReminded(due.map((a) => a.id));
25050
+ console.warn(`[proactive] injection dropped (no main session / guard busy) \u2014 rolled back ${back} action(s) to queued`);
25051
+ return 0;
25052
+ }
24822
25053
  console.log(`[proactive] injected ${payload.length} due action(s) to main session`);
24823
25054
  return payload.length;
24824
25055
  }
@@ -24899,7 +25130,7 @@ function registerProactiveTools() {
24899
25130
  completedAt: nowUtc(),
24900
25131
  cancelReason: String(args.reason || "\u672A\u6CE8\u660E")
24901
25132
  });
24902
- return { content: `\u2713 action ${a.id} \u5DF2\u53D6\u6D88\uFF08${String(args.reason || "\u672A\u6CE8\u660E")}\uFF09` };
25133
+ return { content: `\u2713 action ${a.id} \u6807\u8BB0\u4E3A cancelled\uFF08${String(args.reason || "\u672A\u6CE8\u660E")}\uFF09` };
24903
25134
  });
24904
25135
  return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
24905
25136
  }
@@ -25020,6 +25251,78 @@ var init_completion_evidence = __esm({
25020
25251
  }
25021
25252
  });
25022
25253
 
25254
+ // src/memory/proactive/verdict-parse.ts
25255
+ function parseVerdicts(text) {
25256
+ if (!text) return [];
25257
+ const hits = [];
25258
+ VERDICT_RE.lastIndex = 0;
25259
+ let m;
25260
+ while ((m = VERDICT_RE.exec(text)) !== null) {
25261
+ hits.push({
25262
+ id: m[1],
25263
+ status: m[2],
25264
+ start: m.index,
25265
+ end: m.index + m[0].length
25266
+ });
25267
+ }
25268
+ const byId = /* @__PURE__ */ new Map();
25269
+ for (let i = 0; i < hits.length; i++) {
25270
+ const h = hits[i];
25271
+ const segEnd = Math.min(hits[i + 1]?.start ?? text.length, h.end + SEGMENT_MAX);
25272
+ const seg = text.slice(h.end, segEnd);
25273
+ let reason = "";
25274
+ const inline = seg.match(INLINE_REASON_RE);
25275
+ if (inline) reason = inline[1] ?? inline[2] ?? inline[3] ?? "";
25276
+ if (!reason) {
25277
+ const r = seg.match(LABELED_REASON_RE);
25278
+ if (r) reason = (r[1] ?? r[2] ?? r[3] ?? r[4] ?? "").trim();
25279
+ }
25280
+ byId.set(h.id, { id: h.id, status: h.status, reason: reason.trim().slice(0, SEGMENT_MAX) });
25281
+ }
25282
+ return [...byId.values()];
25283
+ }
25284
+ function applyProseVerdicts(persist, text) {
25285
+ const verdicts = parseVerdicts(text);
25286
+ const skipped = [];
25287
+ let applied = 0;
25288
+ for (const v of verdicts) {
25289
+ const a = persist.getAction(v.id);
25290
+ if (!a) {
25291
+ skipped.push(`${v.id}(\u4E0D\u5B58\u5728)`);
25292
+ continue;
25293
+ }
25294
+ if (a.status !== "executing") {
25295
+ skipped.push(`${v.id}(${a.status})`);
25296
+ continue;
25297
+ }
25298
+ const at = nowUtc();
25299
+ if (v.status === "completed") {
25300
+ persist.updateActionStatus(v.id, "completed", {
25301
+ completedAt: at,
25302
+ ...v.reason ? { note: v.reason } : {}
25303
+ });
25304
+ } else {
25305
+ persist.updateActionStatus(v.id, v.status, {
25306
+ completedAt: at,
25307
+ cancelReason: v.reason || "\u672A\u6CE8\u660E\uFF08\u6B63\u6587\u5224\u5B9A\uFF09"
25308
+ });
25309
+ }
25310
+ applied++;
25311
+ }
25312
+ return { applied, skipped };
25313
+ }
25314
+ var VERDICT_RE, LABELED_REASON_RE, INLINE_REASON_RE, SEGMENT_MAX;
25315
+ var init_verdict_parse = __esm({
25316
+ "src/memory/proactive/verdict-parse.ts"() {
25317
+ "use strict";
25318
+ init_clock();
25319
+ VERDICT_RE = /(act-[0-9a-f]{12})\s*[::]\s*(completed|cancelled|failed)\b/g;
25320
+ LABELED_REASON_RE = /(?:cancel_reason|note|reason)\s*[::]\s*(?:"([^"]*)"|'([^']*)'|“([^”]*)”|([^\n]*))/i;
25321
+ INLINE_REASON_RE = /^\s*[,,]\s*(?:"([^"]*)"|'([^']*)'|“([^”]*)”)/;
25322
+ SEGMENT_MAX = 500;
25323
+ }
25324
+ });
25325
+
25023
25326
  // src/memory/proactive/plugin.ts
25024
25327
  var plugin_exports = {};
25025
25328
  __export(plugin_exports, {
@@ -25027,7 +25330,7 @@ __export(plugin_exports, {
25027
25330
  feedMem7Cell: () => feedMem7Cell
25028
25331
  });
25029
25332
  import { join as join41 } from "node:path";
25030
- import { existsSync as existsSync27, readFileSync as readFileSync33 } from "node:fs";
25333
+ import { existsSync as existsSync27, readFileSync as readFileSync33, appendFileSync as appendFileSync2 } from "node:fs";
25031
25334
  function isFiller(text) {
25032
25335
  const t = text.trim();
25033
25336
  if (t.length >= 5) return false;
@@ -25100,7 +25403,10 @@ var init_plugin = __esm({
25100
25403
  init_proactive_tools();
25101
25404
  init_prompts();
25102
25405
  init_completion_evidence();
25406
+ init_verdict_parse();
25407
+ init_task_manager();
25103
25408
  init_opportunity();
25409
+ init_clock();
25104
25410
  init_embedding();
25105
25411
  init_filters();
25106
25412
  init_store2();
@@ -25262,22 +25568,76 @@ ${String(input.tool_output ?? "")}`
25262
25568
  return { continue: true };
25263
25569
  }
25264
25570
  });
25571
+ registerCallbackHook("Stop", {
25572
+ type: "callback",
25573
+ id: "proactive-verdict-parse",
25574
+ callback: async (input) => {
25575
+ if (input?.channel !== "proactive") return { continue: true };
25576
+ try {
25577
+ const r = applyProseVerdicts(this.persist, String(input?.last_assistant_message ?? ""));
25578
+ if (r.applied > 0) {
25579
+ console.log(`[proactive] \u6B63\u6587\u5224\u5B9A\u56DE\u6536 ${r.applied} \u6761\uFF08\u8DF3\u8FC7 ${r.skipped.length}\uFF09`);
25580
+ }
25581
+ } catch (err) {
25582
+ console.warn(`[proactive] verdict parse failed: ${err.message}`);
25583
+ }
25584
+ return { continue: true };
25585
+ }
25586
+ });
25265
25587
  this.hookInstalled = true;
25266
25588
  activeInstance = this;
25267
25589
  console.log(`[proactive] mem7-fed ingest ready (model=${this.config.modelRef}, graphInput=${liveConfig.get("mem7.graphInput") ?? "memcell"})`);
25268
25590
  registerProactiveTools();
25269
25591
  setProactiveWorkspace(workspace);
25592
+ const pluginCalDb = this.calDb;
25270
25593
  this.sweep = new Sweep({
25271
25594
  persist: this.persist,
25272
25595
  promptFile: join41(workspace, "prompts", "proactive.md"),
25596
+ // 三个都走 getter **每轮现读** —— 热加载规则(config 改了立刻生效)+
25597
+ // calendar 会变,都不能在 start() 时快照。
25598
+ get minLeadDays() {
25599
+ return parseProactiveConfig(liveConfig.get("proactive")).leadGateDays;
25600
+ },
25601
+ get staleHours() {
25602
+ return parseProactiveConfig(liveConfig.get("proactive")).staleHours;
25603
+ },
25604
+ get coveredTitles() {
25605
+ return buildPlanningContext(pluginCalDb, workspace).coveredTitles;
25606
+ },
25607
+ // 带日期时刻的版本 —— 供给"时间锚点判重"(张小媒 0925 review 补的维度)
25608
+ get coveredEvents() {
25609
+ return pluginCalDb ? getCalendarCoverage(pluginCalDb).map((c) => ({ title: c.title, date: c.date, time: c.time })) : [];
25610
+ },
25611
+ // 闸掉的必须留痕:她要能"扫一眼标题,真有冤案我喊"。
25612
+ onGated: (items) => {
25613
+ const gatedPath = join41(workspace, ".proactive", "gated.jsonl");
25614
+ try {
25615
+ appendFileSync2(
25616
+ gatedPath,
25617
+ items.map((i) => JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...i })).join("\n") + "\n",
25618
+ "utf8"
25619
+ );
25620
+ } catch (err) {
25621
+ console.warn(`[proactive] gated.jsonl \u5199\u5165\u5931\u8D25: ${err.message}`);
25622
+ }
25623
+ enqueueNotification(
25624
+ buildNudgeNotification(
25625
+ "stuck",
25626
+ `proactive \u4ECA\u5929\u95F8\u6389\u4E86 ${items.length} \u6761\uFF08\u6CA1\u8FDB\u6CE8\u5165\u3001\u5DF2\u6807 cancelled\uFF09\uFF1A` + items.map((i) => `
25627
+ \xB7 ${i.title.slice(0, 28)} \u2190 ${i.reason.slice(0, 22)}`).join("") + `
25628
+ \u5168\u91CF\u6E05\u5355\uFF1A${gatedPath}\u3002\u89C9\u5F97\u54EA\u6761\u662F\u51A4\u6848\u76F4\u63A5\u8BF4\uFF0C\u6211\u8C03\u9608\u503C\u6216\u6539\u5224\u636E\u3002`
25629
+ ),
25630
+ void 0,
25631
+ `proactive-gated-${todayKey()}`
25632
+ );
25633
+ },
25273
25634
  submit: (blocks) => {
25274
25635
  const mainSessionId = ctx.sessions?.getSessionId?.("scope:main");
25275
25636
  if (!mainSessionId) {
25276
- console.warn("[proactive] no main session \u2014 dropping sweep injection");
25277
- return;
25637
+ console.warn("[proactive] no main session \u2014 sweep injection dropped");
25638
+ return false;
25278
25639
  }
25279
- ;
25280
- ctx.dispatcher.submitMessage({
25640
+ const ok = ctx.dispatcher.submitMessage({
25281
25641
  text: blocks,
25282
25642
  sessionId: mainSessionId,
25283
25643
  channelName: "proactive",
@@ -25286,6 +25646,21 @@ ${String(input.tool_output ?? "")}`
25286
25646
  skipRecall: true,
25287
25647
  deps: ctx.deps
25288
25648
  });
25649
+ if (ok === false) console.warn("[proactive] dispatcher rejected sweep injection (guard busy) \u2014 rolling back");
25650
+ return ok !== false;
25651
+ },
25652
+ // 对账告警:注入的判定连续多轮不闭环 → 直接告诉她。她本人明确要过这条
25653
+ //("能发现只增不减的人是我,但前提是我收到这个信号")——躺日志里等于没有。
25654
+ onUnclosedBacklog: (count) => {
25655
+ enqueueNotification(
25656
+ buildNudgeNotification(
25657
+ "stuck",
25658
+ `proactive \u6709 ${count} \u6761 action \u6302\u7740 executing\u3001\u8FDE\u7EED 30 \u5206\u949F\u53EA\u589E\u4E0D\u51CF\u2014\u2014\u8BF4\u660E\u6CE8\u5165\u7684\u5224\u5B9A\u6CA1\u95ED\u73AF\u3002\u5904\u7406\u5B8C\u8BF7**\u8C03 proactive_done / proactive_cancel \u5DE5\u5177**\uFF08\u5728\u6B63\u6587\u91CC\u5199 act-<id>: cancelled \u53EA\u4F5C\u515C\u5E95\uFF0C\u5DE5\u5177\u8DEF\u5F84\u7684\u7406\u7531\u66F4\u6743\u5A01\uFF09\u3002\u7528 proactive_status \u770B\u5F53\u524D\u6302\u7740\u7684\u5217\u8868\u3002`
25659
+ ),
25660
+ void 0,
25661
+ "proactive-unclosed-backlog"
25662
+ );
25663
+ console.warn(`[proactive] \u5BF9\u8D26\u544A\u8B66\u5DF2\u6CE8\u5165\uFF1Aexecuting=${count} \u8FDE\u7EED ${BACKLOG_ALERT_RUNS} \u8F6E\u53EA\u589E\u4E0D\u51CF`);
25289
25664
  }
25290
25665
  });
25291
25666
  this.sweep.start();
@@ -25311,8 +25686,12 @@ ${String(input.tool_output ?? "")}`
25311
25686
  const nowDate = /* @__PURE__ */ new Date();
25312
25687
  const stateDir = String(liveConfig.get("stateDir") ?? "");
25313
25688
  if (stateDir) {
25314
- const mem7Entries = loadForesightEntries(join41(stateDir, "mem7"), nowDate);
25315
- if (mem7Entries.length > 0) console.log(`[proactive] mem7 foresight: ${mem7Entries.length} open-window predictions`);
25689
+ const all = loadForesightEntries(join41(stateDir, "mem7"), nowDate);
25690
+ const events = all.filter((e) => e.kind !== "profile");
25691
+ const mem7Entries = capForesightEntries(events, todayKey(nowDate));
25692
+ if (all.length > 0) {
25693
+ console.log(`[proactive] mem7 foresight: ${all.length} open-window \u2192 \u5254\u753B\u50CF ${all.length - events.length} \u2192 \u9650\u989D\u540E ${mem7Entries.length} \u8FDB prompt`);
25694
+ }
25316
25695
  for (const e of mem7Entries) {
25317
25696
  const key = e.text.replace(/\s+/g, "");
25318
25697
  if (seen.has(key)) continue;
@@ -25320,7 +25699,6 @@ ${String(input.tool_output ?? "")}`
25320
25699
  foresightLines.push(`[mem7] [\u7A97\u53E3 ${e.start}~${e.end}] ${e.text}${e.evidence ? `\uFF08\u4F9D\u636E\uFF1A${e.evidence}\uFF09` : ""}`);
25321
25700
  }
25322
25701
  }
25323
- if (foresightLines.length > 0) console.log(`[proactive] mem7 foresight total: ${foresightLines.length}`);
25324
25702
  } catch (err) {
25325
25703
  console.warn(`[proactive] foresight load failed: ${err.message}`);
25326
25704
  }
@@ -25344,6 +25722,7 @@ ${String(input.tool_output ?? "")}`
25344
25722
  this.hookInstalled = false;
25345
25723
  if (activeInstance === this) activeInstance = null;
25346
25724
  unregisterCallbackHook("PostToolUse", "proactive-completion-evidence");
25725
+ unregisterCallbackHook("Stop", "proactive-verdict-parse");
25347
25726
  await this.waitForIdle();
25348
25727
  this.sweep?.stop();
25349
25728
  this.sweep = null;
@@ -29643,6 +30022,15 @@ function buildDirTree(v) {
29643
30022
  content: readTemplate("workspace/prompts/contacts.md").replace("{{DISCORD_USER_ID}}", v.discordUserId || "YOUR_DISCORD_ID").replace("{{FEISHU_OPEN_ID}}", v.feishuOpenId || "YOUR_FEISHU_OPEN_ID")
29644
30023
  },
29645
30024
  { path: path34.join(w, "prompts", "auto-memory-instructions.md"), type: "file", content: readTemplate("workspace/prompts/auto-memory-instructions.md") },
30025
+ {
30026
+ // 0925:proactive 到期处理规则。此前仓内**零模板**——那份规则只在私有部署的
30027
+ // stateDir 里手工存在,装机用户吃的是引擎内置兜底文案(那份也没点名工具)。
30028
+ // 结果:agent 用正文写判定、不调工具,判定全部不落库。
30029
+ // 注意 readTemplate 找不到模板会 **throw**,所以本文件与 templates/ 下那份必须成对存在。
30030
+ path: path34.join(w, "prompts", "proactive.md"),
30031
+ type: "file",
30032
+ content: readTemplate("workspace/prompts/proactive.md")
30033
+ },
29646
30034
  {
29647
30035
  // 0917:inner-voice 定制 prompt 按人设装(templates/workspace/inner-voice/<persona>.md)
29648
30036
  path: path34.join(w, "prompts", "my-inner-voice.md"),