@modelzen/feishu-codex-bridge 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +506 -128
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -111,11 +111,13 @@ function getMaxConcurrentRuns(cfg) {
111
111
  function getPendingPolicy(cfg) {
112
112
  return cfg.preferences?.pendingPolicy === "queue" ? "queue" : "steer";
113
113
  }
114
+ var RUN_IDLE_TIMEOUT_MIN_SEC = 10;
115
+ var RUN_IDLE_TIMEOUT_MAX_SEC = 3600;
114
116
  function getRunIdleTimeoutMs(cfg) {
115
117
  const raw = cfg.preferences?.runIdleTimeoutSeconds;
116
118
  if (raw === 0) return void 0;
117
119
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) return 12e4;
118
- const clamped = Math.min(Math.max(Math.floor(raw), 10), 1800);
120
+ const clamped = Math.min(Math.max(Math.floor(raw), RUN_IDLE_TIMEOUT_MIN_SEC), RUN_IDLE_TIMEOUT_MAX_SEC);
119
121
  return clamped * 1e3;
120
122
  }
121
123
  function isChatAllowed(cfg, chatId) {
@@ -598,9 +600,9 @@ async function spawnExecProvider(pc, ref) {
598
600
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
599
601
  return new Promise((resolve7, reject) => {
600
602
  const env = {};
601
- if (pc.passEnv) for (const k of pc.passEnv) {
602
- const v = process.env[k];
603
- if (v) env[k] = v;
603
+ if (pc.passEnv) for (const k2 of pc.passEnv) {
604
+ const v = process.env[k2];
605
+ if (v) env[k2] = v;
604
606
  }
605
607
  if (pc.env) Object.assign(env, pc.env);
606
608
  const child = spawnProcess(pc.command, pc.args ?? [], {
@@ -946,11 +948,11 @@ function emit(level, phase, event, fields = {}) {
946
948
  event,
947
949
  ...ctx
948
950
  };
949
- for (const [k, v] of Object.entries(fields)) {
950
- if (RESERVED_KEYS.has(k)) {
951
- entry[`_${k}`] = v;
951
+ for (const [k2, v] of Object.entries(fields)) {
952
+ if (RESERVED_KEYS.has(k2)) {
953
+ entry[`_${k2}`] = v;
952
954
  } else {
953
- entry[k] = v;
955
+ entry[k2] = v;
954
956
  }
955
957
  }
956
958
  const s = getStream();
@@ -1001,20 +1003,20 @@ function formatFields(fields) {
1001
1003
  const keys = Object.keys(fields);
1002
1004
  if (keys.length === 0) return "";
1003
1005
  const parts = [];
1004
- for (const k of keys) {
1005
- const v = fields[k];
1006
+ for (const k2 of keys) {
1007
+ const v = fields[k2];
1006
1008
  if (v === void 0 || v === null) continue;
1007
- if (k === "stack") continue;
1009
+ if (k2 === "stack") continue;
1008
1010
  if (typeof v === "string") {
1009
- parts.push(`${k}=${v.length > 80 ? `${v.slice(0, 80)}\u2026` : v}`);
1011
+ parts.push(`${k2}=${v.length > 80 ? `${v.slice(0, 80)}\u2026` : v}`);
1010
1012
  } else if (typeof v === "number" || typeof v === "boolean") {
1011
- parts.push(`${k}=${v}`);
1013
+ parts.push(`${k2}=${v}`);
1012
1014
  } else {
1013
1015
  try {
1014
1016
  const str = JSON.stringify(v);
1015
- parts.push(`${k}=${str.length > 80 ? `${str.slice(0, 80)}\u2026` : str}`);
1017
+ parts.push(`${k2}=${str.length > 80 ? `${str.slice(0, 80)}\u2026` : str}`);
1016
1018
  } catch {
1017
- parts.push(`${k}=?`);
1019
+ parts.push(`${k2}=?`);
1018
1020
  }
1019
1021
  }
1020
1022
  }
@@ -1378,6 +1380,14 @@ function mapNotification(n) {
1378
1380
  return mapItemStart(n.params.item);
1379
1381
  case "item/completed":
1380
1382
  return mapItemComplete(n.params.item);
1383
+ case "thread/tokenUsage/updated":
1384
+ return {
1385
+ type: "context_usage",
1386
+ usedTokens: n.params.tokenUsage.last.totalTokens,
1387
+ contextWindow: n.params.tokenUsage.modelContextWindow
1388
+ };
1389
+ case "thread/compacted":
1390
+ return { type: "context_compacted" };
1381
1391
  case "turn/completed":
1382
1392
  return { type: "done", turnId: n.params.turn.id };
1383
1393
  case "error":
@@ -1428,6 +1438,12 @@ function mapItemComplete(item) {
1428
1438
 
1429
1439
  // src/agent/codex-appserver/backend.ts
1430
1440
  var APPROVAL_POLICY = "never";
1441
+ var AUTO_COMPACT_OFF_LIMIT = 1e9;
1442
+ function withAutoCompact(params, autoCompact) {
1443
+ if (autoCompact !== false) return params;
1444
+ const config = params.config ?? {};
1445
+ return { ...params, config: { ...config, model_auto_compact_token_limit: AUTO_COMPACT_OFF_LIMIT } };
1446
+ }
1431
1447
  function sandboxParams(mode, network) {
1432
1448
  if ((mode ?? "full") === "full") return { sandbox: "danger-full-access" };
1433
1449
  if (process.platform !== "darwin" && process.platform !== "win32") {
@@ -1465,6 +1481,7 @@ var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1465
1481
  "\u4E0D\u8981\u624B\u5199\u98DE\u4E66\u5361\u7247\u7684 JSON\u3002\u666E\u901A\u95EE\u7B54\u6B63\u5E38\u56DE\u590D\u5373\u53EF\uFF0C\u53EA\u6709\u7528\u6237\u8981\u5361\u7247\u65F6\u624D\u7528 ```feishu-card \u4EE3\u7801\u5757\u3002"
1466
1482
  ].join("\n");
1467
1483
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1484
+ var COMPACT_TIMEOUT_MS = 12e4;
1468
1485
  function withDeadline(p, ms, label) {
1469
1486
  return new Promise((resolve7, reject) => {
1470
1487
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
@@ -1546,6 +1563,40 @@ var CodexThread = class {
1546
1563
  async abort(turnId) {
1547
1564
  await this.client.request("turn/interrupt", { threadId: this.codexThreadId, turnId });
1548
1565
  }
1566
+ async compact() {
1567
+ let startError;
1568
+ const startFailed = new Promise((resolve7) => {
1569
+ this.client.request("thread/compact/start", { threadId: this.codexThreadId }).then(void 0, (err) => {
1570
+ startError = err instanceof Error ? err : new Error(String(err));
1571
+ log.fail("agent", startError, { phase: "thread/compact/start" });
1572
+ resolve7("start-failed");
1573
+ });
1574
+ });
1575
+ let timer;
1576
+ const timeout = new Promise((resolve7) => {
1577
+ timer = setTimeout(() => resolve7("timeout"), COMPACT_TIMEOUT_MS);
1578
+ });
1579
+ const stream2 = this.client.stream()[Symbol.asyncIterator]();
1580
+ let compacted = false;
1581
+ let usage = null;
1582
+ try {
1583
+ while (true) {
1584
+ const step = await Promise.race([stream2.next(), startFailed, timeout]);
1585
+ if (step === "start-failed") throw startError ?? new Error("thread/compact/start \u8BF7\u6C42\u5931\u8D25");
1586
+ if (step === "timeout") throw new Error(`\u538B\u7F29\u8D85\u65F6\uFF08codex \u672A\u5728 ${COMPACT_TIMEOUT_MS / 1e3}s \u5185\u5B8C\u6210\uFF09`);
1587
+ if (step.done) break;
1588
+ const ev = mapNotification(step.value);
1589
+ if (!ev) continue;
1590
+ if (ev.type === "context_usage") usage = { usedTokens: ev.usedTokens, contextWindow: ev.contextWindow };
1591
+ else if (ev.type === "context_compacted") compacted = true;
1592
+ else if (ev.type === "error" && !ev.willRetry) throw new Error(ev.message);
1593
+ else if (ev.type === "done") break;
1594
+ }
1595
+ } finally {
1596
+ if (timer) clearTimeout(timer);
1597
+ }
1598
+ return { compacted, usage };
1599
+ }
1549
1600
  async close() {
1550
1601
  await this.client.close();
1551
1602
  }
@@ -1634,7 +1685,7 @@ var CodexAppServerBackend = class {
1634
1685
  }
1635
1686
  }
1636
1687
  async startThread(opts) {
1637
- const sandbox = sandboxParams(opts.mode, opts.network);
1688
+ const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
1638
1689
  const client = await this.spawn(opts.cwd);
1639
1690
  const res = await client.request("thread/start", {
1640
1691
  cwd: opts.cwd,
@@ -1646,7 +1697,7 @@ var CodexAppServerBackend = class {
1646
1697
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
1647
1698
  }
1648
1699
  async resumeThread(opts) {
1649
- const sandbox = sandboxParams(opts.mode, opts.network);
1700
+ const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
1650
1701
  const client = await this.spawn(opts.cwd);
1651
1702
  const res = await client.request("thread/resume", {
1652
1703
  threadId: opts.codexThreadId,
@@ -1819,39 +1870,53 @@ function stampRenderToken(card2) {
1819
1870
  }
1820
1871
  }
1821
1872
  }
1822
- for (const k of Object.keys(obj)) visit(obj[k]);
1873
+ for (const k2 of Object.keys(obj)) visit(obj[k2]);
1823
1874
  };
1824
1875
  visit(card2);
1825
1876
  }
1877
+ function isCardIdNotReady(err) {
1878
+ const data = err?.response?.data;
1879
+ return data?.code === 230099 || /11310|cardid is invalid/i.test(data?.msg ?? "");
1880
+ }
1826
1881
  async function sendManagedCard(channel, to, card2, replyTo, replyInThread = false, receiveIdType = "chat_id") {
1827
1882
  stampRenderToken(card2);
1828
- const created = await channel.rawClient.cardkit.v1.card.create({
1829
- data: { type: "card_json", data: JSON.stringify(card2) }
1830
- });
1831
- const cardId = created.data?.card_id;
1832
- if (!cardId) {
1833
- throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
1834
- }
1835
- const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
1836
- let messageId;
1837
- if (replyTo) {
1838
- const sent = await channel.rawClient.im.v1.message.reply({
1839
- path: { message_id: replyTo },
1840
- data: { msg_type: "interactive", content, reply_in_thread: replyInThread }
1841
- });
1842
- messageId = sent.data?.message_id;
1843
- } else {
1844
- const sent = await channel.rawClient.im.v1.message.create({
1845
- params: { receive_id_type: receiveIdType },
1846
- data: { receive_id: to, msg_type: "interactive", content }
1847
- });
1848
- messageId = sent.data?.message_id;
1849
- }
1850
- if (!messageId) {
1851
- throw new Error("send card-by-reference returned no message_id");
1883
+ const data = JSON.stringify(card2);
1884
+ const attempt = async () => {
1885
+ const created = await channel.rawClient.cardkit.v1.card.create({ data: { type: "card_json", data } });
1886
+ const cardId = created.data?.card_id;
1887
+ if (!cardId) {
1888
+ throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
1889
+ }
1890
+ const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
1891
+ let messageId;
1892
+ if (replyTo) {
1893
+ const sent = await channel.rawClient.im.v1.message.reply({
1894
+ path: { message_id: replyTo },
1895
+ data: { msg_type: "interactive", content, reply_in_thread: replyInThread }
1896
+ });
1897
+ messageId = sent.data?.message_id;
1898
+ } else {
1899
+ const sent = await channel.rawClient.im.v1.message.create({
1900
+ params: { receive_id_type: receiveIdType },
1901
+ data: { receive_id: to, msg_type: "interactive", content }
1902
+ });
1903
+ messageId = sent.data?.message_id;
1904
+ }
1905
+ if (!messageId) {
1906
+ throw new Error("send card-by-reference returned no message_id");
1907
+ }
1908
+ byMessageId.set(messageId, { cardId, sequence: 0 });
1909
+ return { messageId, cardId };
1910
+ };
1911
+ for (let i = 0; ; i++) {
1912
+ try {
1913
+ return await attempt();
1914
+ } catch (err) {
1915
+ if (i >= 2 || !isCardIdNotReady(err)) throw err;
1916
+ log.fail("card", err, { phase: "managed-send", attempt: i, retry: true });
1917
+ await new Promise((r) => setTimeout(r, 400 * (i + 1)));
1918
+ }
1852
1919
  }
1853
- byMessageId.set(messageId, { cardId, sequence: 0 });
1854
- return { messageId, cardId };
1855
1920
  }
1856
1921
  async function updateManagedCard(channel, messageId, card2) {
1857
1922
  const entry = byMessageId.get(messageId);
@@ -1981,6 +2046,10 @@ function reduce(state, evt) {
1981
2046
  });
1982
2047
  return { ...state, blocks };
1983
2048
  }
2049
+ case "context_usage":
2050
+ return { ...state, usage: { used: evt.usedTokens, window: evt.contextWindow } };
2051
+ // context_compacted is surfaced as a standalone notice by the run loop, not
2052
+ // folded into the card — fall through to the no-op default.
1984
2053
  case "error":
1985
2054
  return { ...state, terminal: "error", errorMsg: evt.message, footer: null };
1986
2055
  case "done":
@@ -2004,14 +2073,14 @@ function markInterrupted(state) {
2004
2073
  footer: null
2005
2074
  };
2006
2075
  }
2007
- function markIdleTimeout(state, minutes) {
2076
+ function markIdleTimeout(state, seconds) {
2008
2077
  return {
2009
2078
  ...state,
2010
2079
  blocks: closeStreamingText(state.blocks),
2011
2080
  reasoningActive: false,
2012
2081
  terminal: "idle_timeout",
2013
2082
  footer: null,
2014
- idleTimeoutMinutes: minutes
2083
+ idleTimeoutSeconds: seconds
2015
2084
  };
2016
2085
  }
2017
2086
  function finalizeIfRunning(state) {
@@ -2042,8 +2111,8 @@ var RunRender = class {
2042
2111
  return this.state.terminal;
2043
2112
  }
2044
2113
  /** Mark the run as watchdog-killed (idle timeout). */
2045
- timeout(minutes) {
2046
- this.state = markIdleTimeout(this.state, minutes);
2114
+ timeout(seconds) {
2115
+ this.state = markIdleTimeout(this.state, seconds);
2047
2116
  }
2048
2117
  /** Mark the run as user-interrupted (⏹). */
2049
2118
  interrupt() {
@@ -2100,6 +2169,9 @@ function image(imgKey, alt = "") {
2100
2169
  function note(content) {
2101
2170
  return { tag: "div", text: { tag: "lark_md", content, text_size: "notation", text_color: "grey" } };
2102
2171
  }
2172
+ function colorNote(content, color) {
2173
+ return { tag: "div", text: { tag: "lark_md", content, text_size: "notation", text_color: color } };
2174
+ }
2103
2175
  function hr() {
2104
2176
  return { tag: "hr" };
2105
2177
  }
@@ -2318,7 +2390,12 @@ function talkLine(noMention, tail) {
2318
2390
  function buildHelpCard(scope, noMention = true, isAdmin2 = false) {
2319
2391
  const elements = [];
2320
2392
  if (scope === "single") {
2321
- const lines = [talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406"), "\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6"];
2393
+ const lines = [
2394
+ talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406"),
2395
+ "\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6",
2396
+ "\xB7 `/context` \u2192 \u770B\u4E0A\u4E0B\u6587\u5360\u6BD4",
2397
+ "\xB7 `/compact` \u2192 \u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u91CA\u653E\u7A7A\u95F4\uFF09"
2398
+ ];
2322
2399
  if (isAdmin2) lines.push("\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09");
2323
2400
  lines.push("\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361");
2324
2401
  elements.push(md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4** \u2014 \u6574\u7FA4\u5C31\u662F\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\u3002"), hr(), md(lines.join("\n")));
@@ -2329,6 +2406,8 @@ function buildHelpCard(scope, noMention = true, isAdmin2 = false) {
2329
2406
  md(
2330
2407
  `${talkLine(noMention, "\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD")}
2331
2408
  \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2409
+ \xB7 \`/context\` \u2192 \u770B\u4E0A\u4E0B\u6587\u5360\u6BD4
2410
+ \xB7 \`/compact\` \u2192 \u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u91CA\u653E\u7A7A\u95F4\uFF09
2332
2411
  \xB7 \`/help\` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361`
2333
2412
  ),
2334
2413
  note("\u5F00\u65B0\u8BDD\u9898\uFF1A\u56DE\u5230\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9\u3002")
@@ -2363,7 +2442,9 @@ function buildWelcomeCard(kind, docUrl, noMention = true) {
2363
2442
  "\xB7 **@\u6211 + \u5185\u5BB9** \u2192 \u5F00\u4E00\u4E2A\u65B0\u8BDD\u9898\u5E76\u5F00\u59CB\uFF08\u6BCF\u8BDD\u9898\u72EC\u7ACB\u4F1A\u8BDD\uFF09\n\xB7 `/resume` \u2192 \u6062\u590D\u5386\u53F2\u4F1A\u8BDD\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09"
2364
2443
  ),
2365
2444
  md("\u{1F9F5} **\u8BDD\u9898\u5185**"),
2366
- md("\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6"),
2445
+ md(
2446
+ "\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/context` \xB7 `/compact` \u2192 \u770B / \u538B\u7F29\u4E0A\u4E0B\u6587"
2447
+ ),
2367
2448
  note("\u4EFB\u610F\u573A\u666F\u53D1 `/help` \u770B\u5F53\u524D\u53EF\u7528\u547D\u4EE4\u3002")
2368
2449
  );
2369
2450
  }
@@ -2633,7 +2714,80 @@ function escapeInline2(s) {
2633
2714
  return s.replace(/\s+/g, " ").trim();
2634
2715
  }
2635
2716
 
2717
+ // src/card/context-gauge.ts
2718
+ var CTX_WARN = 0.7;
2719
+ var CTX_HIGH = 0.85;
2720
+ var CTX_CRIT = 0.95;
2721
+ function ctxTier(frac) {
2722
+ if (frac >= CTX_CRIT) return { level: 3, color: "red", dot: "\u{1F534}", advice: "\u5F3A\u70C8\u5EFA\u8BAE `/compact` \u538B\u7F29" };
2723
+ if (frac >= CTX_HIGH) return { level: 2, color: "orange", dot: "\u{1F7E0}", advice: "\u5EFA\u8BAE `/compact` \u538B\u7F29" };
2724
+ if (frac >= CTX_WARN) return { level: 1, color: "yellow", dot: "\u{1F7E1}", advice: "\u53EF\u8003\u8651 `/compact` \u538B\u7F29" };
2725
+ return { level: 0, color: "green", dot: "\u{1F7E2}", advice: "" };
2726
+ }
2727
+ function ctxPercent(used, window) {
2728
+ if (!window || window <= 0) return null;
2729
+ return Math.max(0, Math.min(100, Math.round(used / window * 100)));
2730
+ }
2731
+ function k(n) {
2732
+ return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(Math.max(0, Math.round(n)));
2733
+ }
2734
+ function runCardGauge(used, window) {
2735
+ const pct = ctxPercent(used, window);
2736
+ if (pct === null || !window) return null;
2737
+ const frac = used / window;
2738
+ if (frac < CTX_WARN) return null;
2739
+ const t = ctxTier(frac);
2740
+ return colorNote(`${t.dot} \u4E0A\u4E0B\u6587 ${pct}% \xB7 ${k(used)}/${k(window)} \xB7 ${t.advice}`, t.color);
2741
+ }
2742
+ function buildContextCard(used, window) {
2743
+ const pct = ctxPercent(used, window);
2744
+ if (pct === null) {
2745
+ const line = used > 0 ? `\u{1F9E0} \u5DF2\u7528 ${k(used)} tokens\uFF08\u4E0A\u4E0B\u6587\u7A97\u53E3\u672A\u77E5\uFF09` : "\u{1F9E0} \u8FD8\u6CA1\u6709\u7528\u91CF\u6570\u636E\uFF0C\u8DD1\u4E00\u8F6E\u5BF9\u8BDD\u540E\u518D\u770B `/context`\u3002";
2746
+ return card([note(line)], { summary: "\u4E0A\u4E0B\u6587\u7528\u91CF" });
2747
+ }
2748
+ const t = ctxTier(used / window);
2749
+ const els = [colorNote(`${t.dot} **\u4E0A\u4E0B\u6587 ${pct}%** \xB7 ${k(used)}/${k(window)} tokens`, t.color)];
2750
+ els.push(note(t.level >= 1 ? `${t.advice}\uFF1A\u603B\u7ED3\u65E9\u524D\u5BF9\u8BDD\u3001\u91CA\u653E\u7A7A\u95F4\u3002` : "\u7A7A\u95F4\u5145\u8DB3\uFF0C\u65E0\u9700\u538B\u7F29\u3002"));
2751
+ return card(els, { summary: "\u4E0A\u4E0B\u6587\u7528\u91CF" });
2752
+ }
2753
+ var COMPACT_SPINNER = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
2754
+ function buildCompactingCard(tick = 0) {
2755
+ const spin = COMPACT_SPINNER[(tick % COMPACT_SPINNER.length + COMPACT_SPINNER.length) % COMPACT_SPINNER.length];
2756
+ return card([colorNote(`\u{1F5DC}\uFE0F \u6B63\u5728\u538B\u7F29\u4E0A\u4E0B\u6587 ${spin}`, "blue"), note("\u603B\u7ED3\u65E9\u524D\u5BF9\u8BDD\u3001\u91CA\u653E\u7A7A\u95F4\uFF0C\u8BF7\u7A0D\u5019\u3002")], {
2757
+ summary: "\u6B63\u5728\u538B\u7F29\u4E0A\u4E0B\u6587"
2758
+ });
2759
+ }
2760
+ function buildCompactedCard(usage, before) {
2761
+ const els = [colorNote("\u2705 \u4E0A\u4E0B\u6587\u538B\u7F29\u5B8C\u6210", "green")];
2762
+ const pct = usage ? ctxPercent(usage.usedTokens, usage.contextWindow) : null;
2763
+ const dropped = usage != null && before != null && usage.usedTokens < before.used;
2764
+ if (usage && pct !== null && usage.contextWindow && (dropped || before == null)) {
2765
+ const beforePct = before ? ctxPercent(before.used, before.window) : null;
2766
+ const from = dropped && beforePct !== null ? `${beforePct}% \u2192 ` : "";
2767
+ els.push(note(`\u65E9\u524D\u5BF9\u8BDD\u5DF2\u603B\u7ED3\u5F52\u6863\uFF0C\u73B0\u5DF2\u7528 ${from}${pct}%\uFF08${k(usage.usedTokens)}/${k(usage.contextWindow)} tokens\uFF09\u3002`));
2768
+ } else {
2769
+ els.push(note("\u65E9\u524D\u5BF9\u8BDD\u5DF2\u603B\u7ED3\u5F52\u6863\u3001\u817E\u51FA\u7A7A\u95F4\u7EE7\u7EED\uFF1B\u53D1\u4E0B\u4E00\u6761\u6D88\u606F\u540E\uFF0C`/context` \u5373\u53EF\u770B\u5230\u5360\u7528\u4E0B\u964D\u3002"));
2770
+ }
2771
+ return card(els, { summary: "\u4E0A\u4E0B\u6587\u538B\u7F29\u5B8C\u6210" });
2772
+ }
2773
+ function buildCompactFailedCard(message) {
2774
+ return card([colorNote(`\u26A0\uFE0F \u538B\u7F29\u5931\u8D25\uFF1A${message}`, "red")], { summary: "\u538B\u7F29\u5931\u8D25" });
2775
+ }
2776
+ function buildAutoCompactCard() {
2777
+ return card(
2778
+ [
2779
+ hr(),
2780
+ colorNote("\u{1F5DC}\uFE0F \u2500\u2500\u2500 \u4E0A\u4E0B\u6587\u5DF2\u81EA\u52A8\u538B\u7F29 \u2500\u2500\u2500", "blue"),
2781
+ note("\u65E9\u524D\u5BF9\u8BDD\u5DF2\u603B\u7ED3\u5F52\u6863\u3001\u817E\u51FA\u7A7A\u95F4\u7EE7\u7EED\uFF1B\u6700\u8FD1\u7684\u4E0A\u4E0B\u6587\u4FDD\u7559\u3002")
2782
+ ],
2783
+ { summary: "\u4E0A\u4E0B\u6587\u5DF2\u81EA\u52A8\u538B\u7F29" }
2784
+ );
2785
+ }
2786
+
2636
2787
  // src/card/run-card.ts
2788
+ function gaugeEl(state) {
2789
+ return state.usage ? runCardGauge(state.usage.used, state.usage.window) : null;
2790
+ }
2637
2791
  var RC = {
2638
2792
  stop: "run.stop"
2639
2793
  };
@@ -2666,6 +2820,8 @@ function renderRunning(state, rc) {
2666
2820
  if (answer) elements.push(mdStream(answer, ANSWER_EID));
2667
2821
  if (state.footer) elements.push(footerStatus(state.footer));
2668
2822
  if (rc.cardKey) elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
2823
+ const gauge = gaugeEl(state);
2824
+ if (gauge) elements.push(gauge);
2669
2825
  return elements;
2670
2826
  }
2671
2827
  function renderTerminal(state, rc) {
@@ -2691,12 +2847,16 @@ function renderTerminal(state, rc) {
2691
2847
  if (state.terminal === "interrupted") {
2692
2848
  elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
2693
2849
  } else if (state.terminal === "idle_timeout") {
2694
- elements.push(noteMd(`_\u23F1 ${state.idleTimeoutMinutes ?? 0} \u5206\u949F\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
2850
+ const s = state.idleTimeoutSeconds ?? 0;
2851
+ const idleLabel = s > 0 && s % 60 === 0 ? `${s / 60} \u5206\u949F` : `${s} \u79D2`;
2852
+ elements.push(noteMd(`_\u23F1 ${idleLabel}\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
2695
2853
  } else if (state.terminal === "error" && state.errorMsg) {
2696
2854
  elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
2697
2855
  } else if (state.terminal === "done" && !answer) {
2698
2856
  elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
2699
2857
  }
2858
+ const gauge = gaugeEl(state);
2859
+ if (gauge) elements.push(gauge);
2700
2860
  return elements;
2701
2861
  }
2702
2862
  function lastTextIndex(blocks) {
@@ -2910,35 +3070,55 @@ var RunCardStream = class {
2910
3070
  }
2911
3071
  }
2912
3072
  /** Create the entity from the initial (running) card and send a message
2913
- * referencing it by card_id. Returns the carrier message id. */
3073
+ * referencing it by card_id. Returns the carrier message id.
3074
+ *
3075
+ * A just-created CardKit entity occasionally hasn't propagated when the message
3076
+ * referencing it is sent — Feishu 400s with 230099 / ErrCode 11310 "cardid is
3077
+ * invalid" and the run card silently fails to appear (this surfaced as
3078
+ * intermittent intake.fail). Same transient, same fix as
3079
+ * {@link ../card/managed#sendManagedCard}: retry the whole create+send with a
3080
+ * short backoff. Only this transient retries — Feishu rejected the message
3081
+ * outright (nothing sent), and a re-created entity that's never referenced is a
3082
+ * harmless orphan, so no duplicate card. */
2914
3083
  async create(channel, chatId, initialCard, opts) {
2915
- const created = await channel.rawClient.cardkit.v1.card.create({
2916
- data: { type: "card_json", data: JSON.stringify(initialCard) }
2917
- });
2918
- const cardId = created.data?.card_id;
2919
- if (!cardId) {
2920
- throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
2921
- }
2922
- this.cardId = cardId;
2923
- this.lastContent = JSON.stringify(initialCard);
2924
- const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
2925
- let messageId;
2926
- if (opts.replyTo) {
2927
- const r = await channel.rawClient.im.v1.message.reply({
2928
- path: { message_id: opts.replyTo },
2929
- data: { msg_type: "interactive", content, reply_in_thread: opts.replyInThread ?? false }
3084
+ const attempt = async () => {
3085
+ const created = await channel.rawClient.cardkit.v1.card.create({
3086
+ data: { type: "card_json", data: JSON.stringify(initialCard) }
2930
3087
  });
2931
- messageId = r.data?.message_id;
2932
- } else {
2933
- const r = await channel.rawClient.im.v1.message.create({
2934
- params: { receive_id_type: "chat_id" },
2935
- data: { receive_id: chatId, msg_type: "interactive", content }
2936
- });
2937
- messageId = r.data?.message_id;
3088
+ const cardId = created.data?.card_id;
3089
+ if (!cardId) {
3090
+ throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
3091
+ }
3092
+ this.cardId = cardId;
3093
+ this.lastContent = JSON.stringify(initialCard);
3094
+ const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
3095
+ let messageId;
3096
+ if (opts.replyTo) {
3097
+ const r = await channel.rawClient.im.v1.message.reply({
3098
+ path: { message_id: opts.replyTo },
3099
+ data: { msg_type: "interactive", content, reply_in_thread: opts.replyInThread ?? false }
3100
+ });
3101
+ messageId = r.data?.message_id;
3102
+ } else {
3103
+ const r = await channel.rawClient.im.v1.message.create({
3104
+ params: { receive_id_type: "chat_id" },
3105
+ data: { receive_id: chatId, msg_type: "interactive", content }
3106
+ });
3107
+ messageId = r.data?.message_id;
3108
+ }
3109
+ if (!messageId) throw new Error("run card send returned no message_id");
3110
+ this._messageId = messageId;
3111
+ return messageId;
3112
+ };
3113
+ for (let i = 0; ; i++) {
3114
+ try {
3115
+ return await attempt();
3116
+ } catch (err) {
3117
+ if (i >= 2 || !isCardIdNotReady(err)) throw err;
3118
+ log.fail("card", err, { phase: "run-stream-create", attempt: i, retry: true });
3119
+ await new Promise((r) => setTimeout(r, 400 * (i + 1)));
3120
+ }
2938
3121
  }
2939
- if (!messageId) throw new Error("run card send returned no message_id");
2940
- this._messageId = messageId;
2941
- return messageId;
2942
3122
  }
2943
3123
  /** Throttled whole-card stream update. Skips identical/too-soon pushes;
2944
3124
  * `force` flushes regardless (still de-duped on content). */
@@ -3222,8 +3402,8 @@ async function updateProject(name, patch) {
3222
3402
  if (!p) return;
3223
3403
  const actual = typeof patch === "function" ? patch(p) : patch;
3224
3404
  const target = p;
3225
- for (const [k, v] of Object.entries(actual)) {
3226
- if (v !== void 0) target[k] = v;
3405
+ for (const [k2, v] of Object.entries(actual)) {
3406
+ if (v !== void 0) target[k2] = v;
3227
3407
  }
3228
3408
  await write(projects);
3229
3409
  });
@@ -3266,6 +3446,9 @@ var DM = {
3266
3446
  rmCancel: "dm.rmCancel",
3267
3447
  setTools: "dm.set.tools",
3268
3448
  setWatchdog: "dm.set.watchdog",
3449
+ // 假死超时「自定义…」:watchdogCustom 打开输入卡,watchdogCustomSubmit 保存任意秒数
3450
+ watchdogCustom: "dm.set.watchdog.custom",
3451
+ watchdogCustomSubmit: "dm.set.watchdog.customSubmit",
3269
3452
  setPending: "dm.set.pending",
3270
3453
  setConcurrency: "dm.set.concurrency",
3271
3454
  // 权限管理:全局 admins(settings 卡进入)+ 项目响应白名单(项目列表 / 建项目完成卡进入)
@@ -3279,13 +3462,18 @@ var DM = {
3279
3462
  rmAllowed: "dm.allow.rm",
3280
3463
  // 项目设置容器(项目列表 / 建项目完成卡 进入),以后的项目级设置项往这里加
3281
3464
  projectSettings: "dm.projectSettings",
3465
+ // 🧵 话题钻取:项目总览的「🧵 N 话题」按钮 → 该项目话题列表卡
3466
+ projectTopics: "dm.projectTopics",
3282
3467
  setNoMentionDm: "dm.proj.noMention",
3468
+ // 🗜️ 自动压缩:项目级开关(同群设置里的那个,DM 里也能改),按钮携带项目名 n
3469
+ setAutoCompactDm: "dm.proj.autoCompact",
3283
3470
  // 🔐 权限:codex 沙箱档位(管理员档 + 普通用户档)+ 联网,做成下拉表单(选+提交)
3284
3471
  permission: "dm.proj.perm",
3285
3472
  permissionSubmit: "dm.proj.perm.submit"
3286
3473
  };
3287
3474
  var GS = {
3288
- setNoMention: "gs.noMention"
3475
+ setNoMention: "gs.noMention",
3476
+ setAutoCompact: "gs.autoCompact"
3289
3477
  };
3290
3478
  function kindLabel(kind) {
3291
3479
  return kind === "single" ? "\u{1F4AC} \u5355\u4F1A\u8BDD\u7FA4" : "\u{1F465} \u591A\u8BDD\u9898\u7FA4";
@@ -3558,6 +3746,7 @@ function buildNewProjectDoneCard(p) {
3558
3746
  );
3559
3747
  return card(elements, { header: { title, template: "green" } });
3560
3748
  }
3749
+ var PROJECT_TOPICS_MAX = 50;
3561
3750
  function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map()) {
3562
3751
  if (projects.length === 0) {
3563
3752
  return card(
@@ -3567,25 +3756,15 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3567
3756
  }
3568
3757
  const elements = [];
3569
3758
  for (const p of projects) {
3759
+ const topicCount = (p.chatId ? sessionsByChat.get(p.chatId) : void 0)?.length ?? 0;
3760
+ const dir = `\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`;
3761
+ const meta = p.chatId ? `${kindLabel(p.kind)}${(p.origin ?? "created") === "joined" ? " \xB7 \u{1F517}\u5DF2\u52A0\u5165" : ""} \xB7 \u514D@\uFF1A${p.noMention ?? defaultNoMention(p) ? "\u5F00" : "\u5173"}` : "\u26A0\uFE0F \u672A\u7ED1\u5B9A\u7FA4";
3570
3762
  elements.push(md(`**${p.name}**${p.blank ? " _(\u7A7A\u767D)_" : ""}`));
3571
- elements.push(note(`\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`));
3572
- elements.push(
3573
- note(
3574
- p.chatId ? `\u{1F4AC} \u7FA4\uFF1A**${p.name}** \xB7 ${kindLabel(p.kind)}${(p.origin ?? "created") === "joined" ? " \xB7 \u{1F517}\u5DF2\u52A0\u5165" : ""} \xB7 \u514D@\uFF1A${p.noMention ?? defaultNoMention(p) ? "\u5F00" : "\u5173"}` : "\u26A0\uFE0F \u672A\u7ED1\u5B9A\u7FA4"
3575
- )
3576
- );
3577
- const sessions = (p.chatId ? sessionsByChat.get(p.chatId) : void 0) ?? [];
3578
- if (sessions.length === 0) {
3579
- elements.push(note("\uFF08\u6682\u65E0\u8BDD\u9898\uFF09"));
3580
- } else {
3581
- const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
3582
- for (const s of sorted) {
3583
- const title = (s.summary || "(\u7A7A)").replace(/\s+/g, " ").slice(0, 40);
3584
- elements.push(note(`\xB7 ${title} \xB7 ${relativeTime(s.updatedAt)}`));
3585
- }
3586
- }
3763
+ elements.push(note(`${dir}
3764
+ ${meta}`));
3587
3765
  const row = [];
3588
3766
  if (p.chatId) row.push(linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId)));
3767
+ row.push(button(`\u{1F9F5} ${topicCount} \u8BDD\u9898`, { a: DM.projectTopics, n: p.name }));
3589
3768
  row.push(button("\u2699\uFE0F \u8BBE\u7F6E", { a: DM.projectSettings, n: p.name }));
3590
3769
  row.push(button("\u{1F5D1} \u5220\u9664", { a: DM.rmConfirm, n: p.name }, "danger"));
3591
3770
  elements.push(actions(row));
@@ -3595,6 +3774,26 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3595
3774
  elements.push(actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })]));
3596
3775
  return card(elements, { header: { title: "\u{1F4C1} \u9879\u76EE\u5217\u8868", template: "wathet" } });
3597
3776
  }
3777
+ function buildProjectTopicsCard(project, sessions) {
3778
+ const elements = [md(`**${project.name}** \xB7 \u5171 ${sessions.length} \u4E2A\u8BDD\u9898`)];
3779
+ if (sessions.length === 0) {
3780
+ elements.push(note("\uFF08\u6682\u65E0\u8BDD\u9898\uFF09"));
3781
+ } else {
3782
+ const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
3783
+ for (const s of sorted.slice(0, PROJECT_TOPICS_MAX)) {
3784
+ const title = (s.summary || "(\u7A7A)").replace(/\s+/g, " ").slice(0, 50);
3785
+ elements.push(note(`\xB7 ${title} \xB7 ${relativeTime(s.updatedAt)}`));
3786
+ }
3787
+ if (sorted.length > PROJECT_TOPICS_MAX) {
3788
+ elements.push(note(`\xB7 \u2026\u8FD8\u6709 ${sorted.length - PROJECT_TOPICS_MAX} \u4E2A\u8BDD\u9898\uFF08\u66F4\u65E9\u7684\u53EF\u5728\u7FA4\u91CC \`/resume\` \u6062\u590D\uFF09`));
3789
+ }
3790
+ }
3791
+ const nav = [];
3792
+ if (project.chatId) nav.push(linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(project.chatId)));
3793
+ nav.push(button("\u2B05\uFE0F \u9879\u76EE\u5217\u8868", { a: DM.projects }));
3794
+ elements.push(hr(), actions(nav));
3795
+ return card(elements, { header: { title: `\u{1F9F5} \u8BDD\u9898 \xB7 ${project.name}`, template: "wathet" } });
3796
+ }
3598
3797
  function buildRmConfirmCard(name, origin) {
3599
3798
  const note_ = (origin ?? "created") === "joined" ? "\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u786E\u8BA4\u540E**\u6211\u4F1A\u9000\u51FA\u8BE5\u7FA4**\uFF08\u7FA4\u662F\u4F60\u4EEC\u7684\uFF0C\u4E0D\u4F1A\u89E3\u6563\uFF09\u3002" : "\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C + \u64A4\u9500\u7F6E\u9876\u6A2A\u5E45\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u7FA4\u4E3B\u4F1A\u8F6C\u7ED9\u4F60\uFF0C\u518D\u7531\u4F60\u81EA\u884C\u5728\u98DE\u4E66\u89E3\u6563\u7FA4\u3002";
3600
3799
  return card(
@@ -3624,11 +3823,12 @@ function buildSettingsCard(cfg) {
3624
3823
  { label: "\u663E\u793A", value: "on" },
3625
3824
  { label: "\u9690\u85CF", value: "off" }
3626
3825
  ]),
3627
- ...optionRow("\u23F1 \u5047\u6B7B\u8D85\u65F6", DM.setWatchdog, String(watchdogSec), [
3628
- { label: "\u5173\u95ED", value: "0" },
3629
- { label: "60\u79D2", value: "60" },
3630
- { label: "120\u79D2", value: "120" },
3631
- { label: "300\u79D2", value: "300" }
3826
+ md(`\u23F1 \u5047\u6B7B\u8D85\u65F6\uFF08\u5F53\u524D **${watchdogSec === 0 ? "\u5173\u95ED" : `${watchdogSec} \u79D2`}**\uFF09`),
3827
+ actions([
3828
+ ...[0, 120, 300].map(
3829
+ (v) => button(v === 0 ? "\u5173\u95ED" : `${v}\u79D2`, { a: DM.setWatchdog, v: String(v) }, v === watchdogSec ? "primary" : "default")
3830
+ ),
3831
+ button("\u81EA\u5B9A\u4E49\u2026", { a: DM.watchdogCustom })
3632
3832
  ]),
3633
3833
  ...optionRow("\u{1F4E5} \u8FD0\u884C\u4E2D\u65B0\u6D88\u606F", DM.setPending, getPendingPolicy(cfg), [
3634
3834
  { label: "\u5F15\u5BFC", value: "steer" },
@@ -3640,16 +3840,34 @@ function buildSettingsCard(cfg) {
3640
3840
  { label: "10", value: "10" },
3641
3841
  { label: "20", value: "20" }
3642
3842
  ]),
3643
- note("\u26A0\uFE0F \u5047\u6B7B\u8D85\u65F6 / \u5E76\u53D1\u4E0A\u9650 \u6539\u540E\u9700**\u91CD\u542F**\u751F\u6548\uFF1B\u5DE5\u5177\u663E\u793A / \u8FD0\u884C\u4E2D\u65B0\u6D88\u606F \u5373\u65F6\u751F\u6548\u3002"),
3843
+ note("\u26A0\uFE0F \u5E76\u53D1\u4E0A\u9650 \u6539\u540E\u9700**\u91CD\u542F**\u751F\u6548\uFF1B\u5176\u4F59\u8BBE\u7F6E\uFF08\u542B\u5047\u6B7B\u8D85\u65F6\uFF09\u5373\u65F6\u751F\u6548\uFF0C\u6240\u6709\u7FA4\u7ACB\u5373\u5957\u7528\u3002"),
3644
3844
  hr(),
3645
3845
  actions([button("\u{1F46E} \u7BA1\u7406\u5458", { a: DM.admins }), button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })])
3646
3846
  ],
3647
3847
  { header: { title: "\u2699\uFE0F \u8BBE\u7F6E", template: "blue" } }
3648
3848
  );
3649
3849
  }
3850
+ function buildWatchdogCustomCard(cfg) {
3851
+ const cur = cfg.preferences?.runIdleTimeoutSeconds ?? 120;
3852
+ return card(
3853
+ [
3854
+ md("**\u81EA\u5B9A\u4E49\u5047\u6B7B\u8D85\u65F6**"),
3855
+ note(
3856
+ `\u591A\u5C11\u79D2\u6CA1\u6709\u4EFB\u4F55\u8F93\u51FA\u5C31\u81EA\u52A8\u7EC8\u6B62\u672C\u8F6E\u3002\u8303\u56F4 ${RUN_IDLE_TIMEOUT_MIN_SEC}\u2013${RUN_IDLE_TIMEOUT_MAX_SEC} \u79D2\uFF1B\u586B 0 \u5173\u95ED\u3002`
3857
+ ),
3858
+ form("watchdog_custom", [
3859
+ input({ name: "sec", label: "\u8D85\u65F6\u79D2\u6570", placeholder: "\u4F8B\u5982 600", value: String(cur), required: true }),
3860
+ actions([submitButton("\u2705 \u4FDD\u5B58", { a: DM.watchdogCustomSubmit }, "primary", "submit_watchdog")])
3861
+ ]),
3862
+ actions([button("\u2B05\uFE0F \u8FD4\u56DE\u8BBE\u7F6E", { a: DM.settings })])
3863
+ ],
3864
+ { header: { title: "\u23F1 \u81EA\u5B9A\u4E49\u8D85\u65F6", template: "blue" } }
3865
+ );
3866
+ }
3650
3867
  function buildGroupSettingsCard(project) {
3651
3868
  const kind = project.kind ?? "multi";
3652
3869
  const noMention = project.noMention ?? defaultNoMention(project);
3870
+ const autoCompact = project.autoCompact ?? true;
3653
3871
  const scopeNote = kind === "single" ? "\u5F00\u542F\u540E\uFF1A\u672C\u7FA4\u6240\u6709\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\u3002" : "\u5F00\u542F\u540E\uFF1A\u8BDD\u9898\u5185\u7684\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\uFF1B**\u5F00\u65B0\u8BDD\u9898\u4ECD\u9700 @\u6211**\u3002";
3654
3872
  return card(
3655
3873
  [
@@ -3660,7 +3878,12 @@ function buildGroupSettingsCard(project) {
3660
3878
  { label: "\u5173", value: "off" }
3661
3879
  ]),
3662
3880
  note(scopeNote),
3663
- note("\u26A0\uFE0F \u514D@ \u9700\u5E94\u7528\u5DF2\u5F00\u901A\u300C\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\u300D(im:message.group_msg)\u6743\u9650\uFF0C\u5426\u5219\u6536\u4E0D\u5230\u975E @ \u6D88\u606F\u3002")
3881
+ note("\u26A0\uFE0F \u514D@ \u9700\u5E94\u7528\u5DF2\u5F00\u901A\u300C\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\u300D(im:message.group_msg)\u6743\u9650\uFF0C\u5426\u5219\u6536\u4E0D\u5230\u975E @ \u6D88\u606F\u3002"),
3882
+ ...optionRow("\u{1F5DC}\uFE0F \u81EA\u52A8\u538B\u7F29\u4E0A\u4E0B\u6587", GS.setAutoCompact, autoCompact ? "on" : "off", [
3883
+ { label: "\u5F00", value: "on" },
3884
+ { label: "\u5173", value: "off" }
3885
+ ]),
3886
+ note("\u5F00\u542F\u540E\uFF1A\u4E0A\u4E0B\u6587\u63A5\u8FD1\u4E0A\u9650\u65F6 Codex \u81EA\u52A8\u603B\u7ED3\u65E9\u524D\u5BF9\u8BDD\u3001\u91CA\u653E\u7A7A\u95F4\uFF08\u9ED8\u8BA4\u5F00\uFF09\u3002\u6539\u52A8\u4E0B\u4E00\u8F6E\u4F1A\u8BDD\u751F\u6548\u3002")
3664
3887
  ],
3665
3888
  { header: { title: "\u2699\uFE0F \u7FA4\u8BBE\u7F6E", template: "blue" } }
3666
3889
  );
@@ -3778,6 +4001,7 @@ function buildPermissionCard(p) {
3778
4001
  function buildProjectSettingsCard(project) {
3779
4002
  const kind = project.kind ?? "multi";
3780
4003
  const noMention = project.noMention ?? defaultNoMention(project);
4004
+ const autoCompact = project.autoCompact ?? true;
3781
4005
  return card(
3782
4006
  [
3783
4007
  md(`**\u9879\u76EE\u8BBE\u7F6E** \xB7 ${project.name}`),
@@ -3795,6 +4019,13 @@ function buildProjectSettingsCard(project) {
3795
4019
  kind === "single" ? "\u5F00\u542F\u540E\uFF1A\u672C\u7FA4\u6240\u6709\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\u3002" : "\u5F00\u542F\u540E\uFF1A\u8BDD\u9898\u5185\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u5904\u7406\uFF1B**\u5F00\u65B0\u8BDD\u9898\u4ECD\u9700 @\u6211**\u3002"
3796
4020
  ),
3797
4021
  hr(),
4022
+ md("\u{1F5DC}\uFE0F \u81EA\u52A8\u538B\u7F29\u4E0A\u4E0B\u6587"),
4023
+ actions([
4024
+ button("\u5F00", { a: DM.setAutoCompactDm, v: "on", n: project.name }, autoCompact ? "primary" : "default"),
4025
+ button("\u5173", { a: DM.setAutoCompactDm, v: "off", n: project.name }, autoCompact ? "default" : "primary")
4026
+ ]),
4027
+ note("\u5F00\u542F\u540E\uFF1A\u4E0A\u4E0B\u6587\u63A5\u8FD1\u4E0A\u9650\u65F6 Codex \u81EA\u52A8\u603B\u7ED3\u65E9\u524D\u5BF9\u8BDD\u3001\u91CA\u653E\u7A7A\u95F4\uFF08\u9ED8\u8BA4\u5F00\uFF09\u3002\u6539\u52A8\u4E0B\u4E00\u8F6E\u4F1A\u8BDD\u751F\u6548\u3002"),
4028
+ hr(),
3798
4029
  actions([button("\u{1F6E1} \u54CD\u5E94\u767D\u540D\u5355", { a: DM.allowlist, n: project.name }, "primary")]),
3799
4030
  note("\u8BBE\u7F6E\u8C01\u80FD\u8BA9\u6211\u5728\u672C\u7FA4\u54CD\u5E94 / \u8DD1 codex\uFF08\u7A7A = \u6240\u6709\u4EBA\uFF09\u3002"),
3800
4031
  hr(),
@@ -5340,8 +5571,8 @@ async function patchSession(threadId, patch) {
5340
5571
  const rec = sessions.find((s) => s.threadId === threadId);
5341
5572
  if (!rec) return;
5342
5573
  const target = rec;
5343
- for (const [k, v] of Object.entries(patch)) {
5344
- if (v !== void 0) target[k] = v;
5574
+ for (const [k2, v] of Object.entries(patch)) {
5575
+ if (v !== void 0) target[k2] = v;
5345
5576
  }
5346
5577
  rec.updatedAt = Date.now();
5347
5578
  await write2(sessions);
@@ -5459,7 +5690,7 @@ function walkForImageKeys(node, out) {
5459
5690
  }
5460
5691
  const obj = node;
5461
5692
  if (obj.tag === "img" && typeof obj.image_key === "string") out.push(obj.image_key);
5462
- for (const k of Object.keys(obj)) walkForImageKeys(obj[k], out);
5693
+ for (const k2 of Object.keys(obj)) walkForImageKeys(obj[k2], out);
5463
5694
  }
5464
5695
  async function downloadOne(channel, ref, index) {
5465
5696
  try {
@@ -6093,13 +6324,14 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6093
6324
  const active = /* @__PURE__ */ new Map();
6094
6325
  const docLocks = /* @__PURE__ */ new Map();
6095
6326
  const sema = new Semaphore(getMaxConcurrentRuns(cfg));
6096
- const idleMs = getRunIdleTimeoutMs(cfg) ?? 0;
6327
+ const currentIdleMs = () => getRunIdleTimeoutMs(cfg) ?? 0;
6097
6328
  const resumePending = /* @__PURE__ */ new Map();
6098
6329
  const modelPending = /* @__PURE__ */ new Map();
6099
6330
  const runsByCard = /* @__PURE__ */ new Map();
6100
6331
  const runCards = /* @__PURE__ */ new Map();
6101
6332
  const runStreams = /* @__PURE__ */ new Map();
6102
6333
  const lastRunCard = /* @__PURE__ */ new Map();
6334
+ const lastUsage = /* @__PURE__ */ new Map();
6103
6335
  let modelsCache = null;
6104
6336
  async function listModels() {
6105
6337
  if (!modelsCache) modelsCache = await backend.listModels();
@@ -6195,6 +6427,14 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6195
6427
  await postModelCard(msg, ts.sessionKey);
6196
6428
  return;
6197
6429
  }
6430
+ if (cmd === "compact") {
6431
+ await runCompact(msg, ts.sessionKey, false, ts);
6432
+ return;
6433
+ }
6434
+ if (cmd === "context") {
6435
+ await postContextCard(msg, ts.sessionKey, false);
6436
+ return;
6437
+ }
6198
6438
  handleTurn(msg, text, ts.sessionKey, true, project, ts);
6199
6439
  return;
6200
6440
  }
@@ -6208,6 +6448,14 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6208
6448
  await postModelCard(msg, ts.sessionKey);
6209
6449
  return;
6210
6450
  }
6451
+ if (cmd === "compact") {
6452
+ await runCompact(msg, ts.sessionKey, true, ts);
6453
+ return;
6454
+ }
6455
+ if (cmd === "context") {
6456
+ await postContextCard(msg, ts.sessionKey, true);
6457
+ return;
6458
+ }
6211
6459
  handleTurn(msg, text, ts.sessionKey, false, project, ts);
6212
6460
  return;
6213
6461
  }
@@ -6223,8 +6471,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6223
6471
  await postGroupSettings(msg, project);
6224
6472
  return;
6225
6473
  }
6226
- if (cmd === "model") {
6227
- await channel.send(msg.chatId, { markdown: "`/model` \u9700\u8981\u5728\u8BDD\u9898\u91CC\u4F7F\u7528\uFF08\u5148 @\u6211 \u5F00\u4E2A\u8BDD\u9898\uFF09\u3002" }, { replyTo: msg.messageId }).catch(() => void 0);
6474
+ if (cmd === "model" || cmd === "compact" || cmd === "context") {
6475
+ await channel.send(msg.chatId, { markdown: `\`/${cmd}\` \u9700\u8981\u5728\u8BDD\u9898\u91CC\u4F7F\u7528\uFF08\u5148 @\u6211 \u5F00\u4E2A\u8BDD\u9898\uFF09\u3002` }, { replyTo: msg.messageId }).catch(() => void 0);
6228
6476
  return;
6229
6477
  }
6230
6478
  startTopicDirectly(msg, text, project);
@@ -6232,7 +6480,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6232
6480
  function parseCommand(text) {
6233
6481
  const m = /^\/(\w+)/.exec(text);
6234
6482
  const name = m?.[1]?.toLowerCase();
6235
- return name === "resume" || name === "model" || name === "settings" || name === "help" ? name : null;
6483
+ return name === "resume" || name === "model" || name === "settings" || name === "help" || name === "compact" || name === "context" ? name : null;
6236
6484
  }
6237
6485
  function shouldRespondWithoutMention(project, msg) {
6238
6486
  if (!(project.noMention ?? defaultNoMention(project))) return false;
@@ -6261,7 +6509,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6261
6509
  function turnPerm(project, senderId) {
6262
6510
  if (!project) return {};
6263
6511
  const t = turnTier(project, isAdmin(cfg, senderId));
6264
- return { mode: t.mode, network: project.network, roleSuffix: t.split ? t.role : void 0 };
6512
+ return { mode: t.mode, network: project.network, autoCompact: project.autoCompact, roleSuffix: t.split ? t.role : void 0 };
6265
6513
  }
6266
6514
  function turnSession(baseKey, project, senderId) {
6267
6515
  const perm = turnPerm(project, senderId);
@@ -6326,7 +6574,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6326
6574
  let firstText = preIngested ? text : await ingestContext(msg, text);
6327
6575
  const { thread: resolved, recreated } = await resolveThread(sessionKey, msg.chatId, {
6328
6576
  mode: perm.mode,
6329
- network: perm.network
6577
+ network: perm.network,
6578
+ autoCompact: perm.autoCompact
6330
6579
  });
6331
6580
  let thread = resolved;
6332
6581
  const neverSeen = !thread;
@@ -6334,7 +6583,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6334
6583
  const prior = neverSeen ? void 0 : await getSession(sessionKey);
6335
6584
  if (!thread) {
6336
6585
  const cwd = project?.cwd ?? fallbackCwd;
6337
- thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
6586
+ thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network, autoCompact: perm.autoCompact });
6338
6587
  sessions.set(sessionKey, thread);
6339
6588
  await upsertSession({
6340
6589
  threadId: sessionKey,
@@ -6393,7 +6642,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6393
6642
  model: rec.model,
6394
6643
  effort: rec.effort,
6395
6644
  mode: perm?.mode,
6396
- network: perm?.network
6645
+ network: perm?.network,
6646
+ autoCompact: perm?.autoCompact
6397
6647
  });
6398
6648
  sessions.set(threadId, resumed);
6399
6649
  return { thread: resumed, recreated: false };
@@ -6406,7 +6656,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6406
6656
  model: rec.model,
6407
6657
  effort: rec.effort,
6408
6658
  mode: perm?.mode ?? project?.mode,
6409
- network: perm?.network ?? project?.network
6659
+ network: perm?.network ?? project?.network,
6660
+ autoCompact: perm?.autoCompact ?? project?.autoCompact
6410
6661
  });
6411
6662
  sessions.set(threadId, fresh);
6412
6663
  await patchSession(threadId, { codexThreadId: fresh.codexThreadId }).catch(() => void 0);
@@ -6434,7 +6685,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6434
6685
  const { model, effort } = pickDefault(await listModels());
6435
6686
  let thread;
6436
6687
  try {
6437
- thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network });
6688
+ thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network, autoCompact: perm.autoCompact });
6438
6689
  } catch (err) {
6439
6690
  reaction.done();
6440
6691
  log.fail("card", err, { phase: "start-topic" });
@@ -6508,6 +6759,76 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6508
6759
  log.info("card", "model", { threadId: sessionKey, model: state.model, effort: state.effort });
6509
6760
  });
6510
6761
  }
6762
+ async function postContextCard(msg, sessionKey, inThread) {
6763
+ const u = lastUsage.get(sessionKey);
6764
+ await sendManagedCard(channel, msg.chatId, buildContextCard(u?.used ?? 0, u?.window ?? null), msg.messageId, inThread).catch(
6765
+ (err) => log.fail("card", err, { phase: "context" })
6766
+ );
6767
+ }
6768
+ const COMPACT_ANIM_INTERVAL_MS = 800;
6769
+ async function runCompact(msg, sessionKey, inThread, perm) {
6770
+ const reply = (markdown) => channel.send(msg.chatId, { markdown }, { replyTo: msg.messageId, replyInThread: inThread }).then(() => void 0, () => void 0);
6771
+ if (active.get(sessionKey)) {
6772
+ await reply("\u23F3 \u8FD9\u4E00\u8F6E\u8FD8\u5728\u8DD1\uFF0C\u7ED3\u675F\u540E\u518D `/compact`\u3002");
6773
+ return;
6774
+ }
6775
+ const { thread } = await resolveThread(sessionKey, msg.chatId, {
6776
+ mode: perm.mode,
6777
+ network: perm.network,
6778
+ autoCompact: perm.autoCompact
6779
+ });
6780
+ if (!thread) {
6781
+ await reply("\u8FD9\u4E2A\u4F1A\u8BDD\u8FD8\u6CA1\u5F00\u59CB\uFF0C\u5148\u53D1\u6761\u6D88\u606F\u804A\u4E24\u53E5\u518D `/compact`\u3002");
6782
+ return;
6783
+ }
6784
+ let cardMsgId;
6785
+ try {
6786
+ const sent = await sendManagedCard(channel, msg.chatId, buildCompactingCard(0), msg.messageId, inThread);
6787
+ cardMsgId = sent.messageId;
6788
+ } catch (err) {
6789
+ log.fail("card", err, { phase: "compact-start-card" });
6790
+ }
6791
+ let stop = false;
6792
+ const wakers = [];
6793
+ const sleep = (ms) => new Promise((res) => {
6794
+ const t = setTimeout(res, ms);
6795
+ wakers.push(() => {
6796
+ clearTimeout(t);
6797
+ res();
6798
+ });
6799
+ });
6800
+ const anim = (async () => {
6801
+ let tick = 0;
6802
+ while (!stop && cardMsgId) {
6803
+ await sleep(COMPACT_ANIM_INTERVAL_MS);
6804
+ if (stop || !cardMsgId) break;
6805
+ tick++;
6806
+ await updateManagedCard(channel, cardMsgId, buildCompactingCard(tick)).catch(() => void 0);
6807
+ }
6808
+ })();
6809
+ const settle = async (result) => {
6810
+ stop = true;
6811
+ wakers.forEach((w) => w());
6812
+ await anim;
6813
+ if (cardMsgId && await updateManagedCard(channel, cardMsgId, result)) return;
6814
+ await sendManagedCard(channel, msg.chatId, result, msg.messageId, inThread).catch(
6815
+ (err) => log.fail("card", err, { phase: "compact-settle" })
6816
+ );
6817
+ };
6818
+ const before = lastUsage.get(sessionKey) ?? null;
6819
+ try {
6820
+ const { usage } = await thread.compact();
6821
+ if (usage) lastUsage.set(sessionKey, { used: usage.usedTokens, window: usage.contextWindow });
6822
+ else lastUsage.delete(sessionKey);
6823
+ log.info("intake", "compact", { sessionKey, used: usage?.usedTokens ?? null, before: before?.used ?? null });
6824
+ await settle(buildCompactedCard(usage, before));
6825
+ } catch (err) {
6826
+ const m = err instanceof Error ? err.message : String(err);
6827
+ const unsupported = /method not found|-32601|unknown (method|request)/i.test(m);
6828
+ log.fail("intake", err, { phase: "compact" });
6829
+ await settle(buildCompactFailedCard(unsupported ? "\u5F53\u524D codex \u7248\u672C\u4E0D\u652F\u6301 /compact\uFF0C\u8BF7\u5347\u7EA7\u540E\u518D\u8BD5\u3002" : m));
6830
+ }
6831
+ }
6511
6832
  async function postHelpCard(msg, scope, inThread = false, project) {
6512
6833
  const noMention = project ? project.noMention ?? defaultNoMention(project) : true;
6513
6834
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
@@ -6523,24 +6844,26 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6523
6844
  const settleUpdate = (msgId, c, fallbackChatId) => {
6524
6845
  const armedAt = Date.now();
6525
6846
  void (async () => {
6526
- await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
6527
- const card2 = typeof c === "function" ? await c() : c;
6528
- const ok = await updateManagedCard(channel, msgId, card2);
6529
- log.info("console", "settle-update", { msgId, ok, waitedMs: Date.now() - armedAt, fallback: !ok && !!fallbackChatId });
6530
- if (!ok && fallbackChatId) {
6531
- await sendManagedCard(channel, fallbackChatId, card2).catch(
6532
- (err) => log.fail("console", err, { phase: "settle-fallback" })
6533
- );
6847
+ try {
6848
+ await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
6849
+ const card2 = typeof c === "function" ? await c() : c;
6850
+ const ok = await updateManagedCard(channel, msgId, card2);
6851
+ log.info("console", "settle-update", { msgId, ok, waitedMs: Date.now() - armedAt, fallback: !ok && !!fallbackChatId });
6852
+ if (!ok && fallbackChatId) {
6853
+ await sendManagedCard(channel, fallbackChatId, card2);
6854
+ }
6855
+ } catch (err) {
6856
+ log.fail("console", err, { phase: "settle-update", msgId });
6534
6857
  }
6535
6858
  })();
6536
6859
  };
6537
6860
  function pruneResumePending() {
6538
6861
  const now = Date.now();
6539
- for (const [k, s] of resumePending) if (now - s.createdAt > PENDING_TTL_MS) resumePending.delete(k);
6862
+ for (const [k2, s] of resumePending) if (now - s.createdAt > PENDING_TTL_MS) resumePending.delete(k2);
6540
6863
  }
6541
6864
  function pruneModelPending() {
6542
6865
  const now = Date.now();
6543
- for (const [k, s] of modelPending) if (now - s.createdAt > PENDING_TTL_MS) modelPending.delete(k);
6866
+ for (const [k2, s] of modelPending) if (now - s.createdAt > PENDING_TTL_MS) modelPending.delete(k2);
6544
6867
  }
6545
6868
  function authPending(map, evt) {
6546
6869
  const state = map.get(evt.messageId);
@@ -6875,6 +7198,18 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
6875
7198
  }).on(DM.setWatchdog, ({ evt, value }) => {
6876
7199
  const n = Number(value.v);
6877
7200
  if (Number.isFinite(n)) applyPref(evt, (p) => p.runIdleTimeoutSeconds = n);
7201
+ }).on(DM.watchdogCustom, ({ evt }) => {
7202
+ if (!dmAdmin(evt.operator?.openId)) return;
7203
+ void patch(evt, buildWatchdogCustomCard(cfg));
7204
+ }).on(DM.watchdogCustomSubmit, ({ evt, formValue }) => {
7205
+ const raw = String(formValue?.sec ?? "").trim();
7206
+ const n = Number(raw);
7207
+ if (!Number.isFinite(n) || n < 0) {
7208
+ void patch(evt, buildWatchdogCustomCard(cfg));
7209
+ return;
7210
+ }
7211
+ const sec = n === 0 ? 0 : Math.min(Math.max(Math.floor(n), RUN_IDLE_TIMEOUT_MIN_SEC), RUN_IDLE_TIMEOUT_MAX_SEC);
7212
+ applyPref(evt, (p) => p.runIdleTimeoutSeconds = sec);
6878
7213
  }).on(DM.setPending, ({ evt, value }) => {
6879
7214
  if (value.v === "steer" || value.v === "queue") applyPref(evt, (p) => p.pendingPolicy = value.v);
6880
7215
  }).on(DM.setConcurrency, ({ evt, value }) => {
@@ -6892,6 +7227,19 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
6892
7227
  }
6893
7228
  return buildGroupSettingsCard({ name: "\u672C\u7FA4", kind: "multi", noMention: on });
6894
7229
  });
7230
+ }).on(GS.setAutoCompact, ({ evt, value }) => {
7231
+ if (!isAdmin(cfg, evt.operator?.openId ?? "")) return;
7232
+ const on = value.v === "on";
7233
+ patch(evt, async () => {
7234
+ const project = await getProjectByChatId(evt.chatId);
7235
+ if (project) {
7236
+ await updateProject(project.name, { autoCompact: on });
7237
+ await evictLiveSessionsForChat(project.chatId);
7238
+ log.info("console", "group-autocompact", { project: project.name, on });
7239
+ return buildGroupSettingsCard({ ...project, autoCompact: on });
7240
+ }
7241
+ return buildGroupSettingsCard({ name: "\u672C\u7FA4", kind: "multi", autoCompact: on });
7242
+ });
6895
7243
  }).on(DM.admins, ({ evt }) => {
6896
7244
  if (!dmAdmin(evt.operator?.openId)) return;
6897
7245
  patch(
@@ -6983,6 +7331,15 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
6983
7331
  const p = await getProjectByName(name);
6984
7332
  return p ? buildProjectSettingsCard(p) : buildDmMenuCard();
6985
7333
  });
7334
+ }).on(DM.projectTopics, ({ evt, value }) => {
7335
+ if (!dmAdmin(evt.operator?.openId)) return;
7336
+ const name = typeof value.n === "string" ? value.n : "";
7337
+ patch(evt, async () => {
7338
+ const p = await getProjectByName(name);
7339
+ if (!p) return buildDmMenuCard();
7340
+ const sessions2 = (await listSessions()).filter((s) => s.chatId === p.chatId);
7341
+ return buildProjectTopicsCard(p, sessions2);
7342
+ });
6986
7343
  }).on(DM.setNoMentionDm, ({ evt, value }) => {
6987
7344
  if (!dmAdmin(evt.operator?.openId)) return;
6988
7345
  const name = typeof value.n === "string" ? value.n : "";
@@ -6993,6 +7350,18 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
6993
7350
  await updateProject(name, { noMention: on });
6994
7351
  return buildProjectSettingsCard({ ...p, noMention: on });
6995
7352
  });
7353
+ }).on(DM.setAutoCompactDm, ({ evt, value }) => {
7354
+ if (!dmAdmin(evt.operator?.openId)) return;
7355
+ const name = typeof value.n === "string" ? value.n : "";
7356
+ const on = value.v === "on";
7357
+ patch(evt, async () => {
7358
+ const p = await getProjectByName(name);
7359
+ if (!p) return buildDmMenuCard();
7360
+ await updateProject(name, { autoCompact: on });
7361
+ await evictLiveSessionsForChat(p.chatId);
7362
+ log.info("console", "project-autocompact", { project: name, on });
7363
+ return buildProjectSettingsCard({ ...p, autoCompact: on });
7364
+ });
6996
7365
  }).on(DM.permission, ({ evt, value }) => {
6997
7366
  if (!dmAdmin(evt.operator?.openId)) return;
6998
7367
  const name = typeof value.n === "string" ? value.n : "";
@@ -7158,6 +7527,7 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7158
7527
  interrupted = true;
7159
7528
  resolveStop();
7160
7529
  };
7530
+ const idleMs = currentIdleMs();
7161
7531
  const guarded = withIdleTimeout(
7162
7532
  run.events,
7163
7533
  idleMs,
@@ -7183,6 +7553,14 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7183
7553
  }
7184
7554
  lastEvAt = tEv;
7185
7555
  evCount++;
7556
+ if (et === "context_usage" && topicThreadId) {
7557
+ const cu = ev;
7558
+ lastUsage.set(topicThreadId, { used: cu.usedTokens, window: cu.contextWindow });
7559
+ } else if (et === "context_compacted") {
7560
+ void sendManagedCard(channel, opts.chatId, buildAutoCompactCard(), cardMsgId, !opts.flat).catch(
7561
+ (err) => log.fail("card", err, { phase: "auto-compact-notice" })
7562
+ );
7563
+ }
7186
7564
  render.apply(ev);
7187
7565
  rc.rs = render.snapshot();
7188
7566
  stream2.streamCoalesced(channel, buildRunCard(rc), ANSWER_EID);
@@ -7191,7 +7569,7 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7191
7569
  await stream2.drain();
7192
7570
  state.interrupt = void 0;
7193
7571
  const killed = interrupted || timedOut;
7194
- if (timedOut) render.timeout(Math.max(1, Math.round(idleMs / 6e4)));
7572
+ if (timedOut) render.timeout(Math.round(idleMs / 1e3));
7195
7573
  else if (interrupted) render.interrupt();
7196
7574
  else render.finalize();
7197
7575
  rc.rs = render.snapshot();
@@ -7284,7 +7662,7 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7284
7662
  const run = thread.runStreamed({ text: prompt }, { model: rec?.model, effort: rec?.effort });
7285
7663
  let state = initialState;
7286
7664
  let timedOut = false;
7287
- const guarded = withIdleTimeout(run.events, idleMs, () => {
7665
+ const guarded = withIdleTimeout(run.events, currentIdleMs(), () => {
7288
7666
  timedOut = true;
7289
7667
  });
7290
7668
  for await (const ev of guarded) state = reduce(state, ev);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {