@modelzen/feishu-codex-bridge 0.3.8 → 0.3.10

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 +826 -157
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -600,9 +600,9 @@ async function spawnExecProvider(pc, ref) {
600
600
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
601
601
  return new Promise((resolve7, reject) => {
602
602
  const env = {};
603
- if (pc.passEnv) for (const k of pc.passEnv) {
604
- const v = process.env[k];
605
- 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;
606
606
  }
607
607
  if (pc.env) Object.assign(env, pc.env);
608
608
  const child = spawnProcess(pc.command, pc.args ?? [], {
@@ -948,11 +948,11 @@ function emit(level, phase, event, fields = {}) {
948
948
  event,
949
949
  ...ctx
950
950
  };
951
- for (const [k, v] of Object.entries(fields)) {
952
- if (RESERVED_KEYS.has(k)) {
953
- entry[`_${k}`] = v;
951
+ for (const [k2, v] of Object.entries(fields)) {
952
+ if (RESERVED_KEYS.has(k2)) {
953
+ entry[`_${k2}`] = v;
954
954
  } else {
955
- entry[k] = v;
955
+ entry[k2] = v;
956
956
  }
957
957
  }
958
958
  const s = getStream();
@@ -1003,20 +1003,20 @@ function formatFields(fields) {
1003
1003
  const keys = Object.keys(fields);
1004
1004
  if (keys.length === 0) return "";
1005
1005
  const parts = [];
1006
- for (const k of keys) {
1007
- const v = fields[k];
1006
+ for (const k2 of keys) {
1007
+ const v = fields[k2];
1008
1008
  if (v === void 0 || v === null) continue;
1009
- if (k === "stack") continue;
1009
+ if (k2 === "stack") continue;
1010
1010
  if (typeof v === "string") {
1011
- 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}`);
1012
1012
  } else if (typeof v === "number" || typeof v === "boolean") {
1013
- parts.push(`${k}=${v}`);
1013
+ parts.push(`${k2}=${v}`);
1014
1014
  } else {
1015
1015
  try {
1016
1016
  const str = JSON.stringify(v);
1017
- 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}`);
1018
1018
  } catch {
1019
- parts.push(`${k}=?`);
1019
+ parts.push(`${k2}=?`);
1020
1020
  }
1021
1021
  }
1022
1022
  }
@@ -1189,6 +1189,14 @@ ${rule}`);
1189
1189
  // src/bot/bridge.ts
1190
1190
  import { createLarkChannel, Domain } from "@larksuiteoapi/node-sdk";
1191
1191
 
1192
+ // src/agent/types.ts
1193
+ function isGoalTerminal(status) {
1194
+ return status === "complete" || status === "budgetLimited" || status === "usageLimited" || status === "blocked";
1195
+ }
1196
+ function isGoalSuccess(status) {
1197
+ return status === "complete";
1198
+ }
1199
+
1192
1200
  // src/agent/codex-appserver/app-server-client.ts
1193
1201
  var AsyncQueue = class {
1194
1202
  items = [];
@@ -1252,7 +1260,11 @@ var AppServerClient = class {
1252
1260
  child.on("error", (err) => this.failAllPending(err));
1253
1261
  await this.request("initialize", {
1254
1262
  clientInfo: { name: this.opts.clientName ?? "feishu-codex-bridge", version: "0.0.1" },
1255
- capabilities: null
1263
+ // experimentalApi opts into experimental JSON-RPC methods + fields — REQUIRED
1264
+ // for the goal RPCs (thread/goal/set|get|clear). Verified against codex 0.139:
1265
+ // without it, thread/goal/set is rejected. The `goals` feature itself is
1266
+ // stable+on by default there, so no experimentalFeature/enablement/set needed.
1267
+ capabilities: { experimentalApi: true, requestAttestation: false }
1256
1268
  });
1257
1269
  this.notify("initialized");
1258
1270
  }
@@ -1380,8 +1392,28 @@ function mapNotification(n) {
1380
1392
  return mapItemStart(n.params.item);
1381
1393
  case "item/completed":
1382
1394
  return mapItemComplete(n.params.item);
1395
+ case "thread/tokenUsage/updated":
1396
+ return {
1397
+ type: "context_usage",
1398
+ usedTokens: n.params.tokenUsage.last.totalTokens,
1399
+ contextWindow: n.params.tokenUsage.modelContextWindow
1400
+ };
1401
+ case "thread/compacted":
1402
+ return { type: "context_compacted" };
1383
1403
  case "turn/completed":
1384
1404
  return { type: "done", turnId: n.params.turn.id };
1405
+ case "thread/goal/updated": {
1406
+ const g = n.params.goal;
1407
+ return {
1408
+ type: "goal_update",
1409
+ status: g.status,
1410
+ objective: g.objective,
1411
+ tokensUsed: g.tokensUsed,
1412
+ timeUsedSeconds: g.timeUsedSeconds,
1413
+ tokenBudget: g.tokenBudget
1414
+ };
1415
+ }
1416
+ // thread/goal/cleared — we clear goals ourselves; nothing to surface.
1385
1417
  case "error":
1386
1418
  return { type: "error", message: n.params.error.message, willRetry: n.params.willRetry };
1387
1419
  default:
@@ -1430,6 +1462,12 @@ function mapItemComplete(item) {
1430
1462
 
1431
1463
  // src/agent/codex-appserver/backend.ts
1432
1464
  var APPROVAL_POLICY = "never";
1465
+ var AUTO_COMPACT_OFF_LIMIT = 1e9;
1466
+ function withAutoCompact(params, autoCompact) {
1467
+ if (autoCompact !== false) return params;
1468
+ const config = params.config ?? {};
1469
+ return { ...params, config: { ...config, model_auto_compact_token_limit: AUTO_COMPACT_OFF_LIMIT } };
1470
+ }
1433
1471
  function sandboxParams(mode, network) {
1434
1472
  if ((mode ?? "full") === "full") return { sandbox: "danger-full-access" };
1435
1473
  if (process.platform !== "darwin" && process.platform !== "win32") {
@@ -1467,6 +1505,7 @@ var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1467
1505
  "\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"
1468
1506
  ].join("\n");
1469
1507
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1508
+ var COMPACT_TIMEOUT_MS = 12e4;
1470
1509
  function withDeadline(p, ms, label) {
1471
1510
  return new Promise((resolve7, reject) => {
1472
1511
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
@@ -1538,6 +1577,64 @@ var CodexThread = class {
1538
1577
  }
1539
1578
  return { events: gen(), turnId: () => self.currentTurnId };
1540
1579
  }
1580
+ runGoal(objective) {
1581
+ const self = this;
1582
+ this.currentTurnId = void 0;
1583
+ async function* gen() {
1584
+ await self.client.request("thread/goal/clear", { threadId: self.codexThreadId }).catch(() => void 0);
1585
+ let setError;
1586
+ const setFailed = new Promise((resolve7) => {
1587
+ self.client.request("thread/goal/set", { threadId: self.codexThreadId, objective }).then(void 0, (err) => {
1588
+ setError = err instanceof Error ? err : new Error(String(err));
1589
+ log.fail("agent", setError, { phase: "thread/goal/set" });
1590
+ resolve7("set-failed");
1591
+ });
1592
+ });
1593
+ const stream2 = self.client.stream()[Symbol.asyncIterator]();
1594
+ let armed = false;
1595
+ let turnActive = false;
1596
+ let goalDone = false;
1597
+ while (true) {
1598
+ const step = await Promise.race([stream2.next(), setFailed]);
1599
+ if (step === "set-failed") {
1600
+ yield { type: "error", message: setError?.message ?? "thread/goal/set \u8BF7\u6C42\u5931\u8D25", willRetry: false };
1601
+ return;
1602
+ }
1603
+ if (step.done) return;
1604
+ const ev = mapNotification(step.value);
1605
+ if (!ev) continue;
1606
+ if (ev.type === "turn_started") {
1607
+ self.currentTurnId = ev.turnId;
1608
+ armed = true;
1609
+ turnActive = true;
1610
+ yield ev;
1611
+ continue;
1612
+ }
1613
+ if (ev.type === "done") {
1614
+ turnActive = false;
1615
+ yield ev;
1616
+ if (goalDone) return;
1617
+ continue;
1618
+ }
1619
+ if (ev.type === "goal_update") {
1620
+ if (ev.objective !== objective) continue;
1621
+ if (ev.status === "active" || ev.status === "paused") armed = true;
1622
+ yield ev;
1623
+ if (armed && isGoalTerminal(ev.status)) {
1624
+ if (turnActive) goalDone = true;
1625
+ else return;
1626
+ }
1627
+ continue;
1628
+ }
1629
+ yield ev;
1630
+ if (ev.type === "error" && !ev.willRetry) return;
1631
+ }
1632
+ }
1633
+ return { events: gen(), turnId: () => self.currentTurnId };
1634
+ }
1635
+ async clearGoal() {
1636
+ await this.client.request("thread/goal/clear", { threadId: this.codexThreadId });
1637
+ }
1541
1638
  async steer(input2, expectedTurnId) {
1542
1639
  await this.client.request("turn/steer", {
1543
1640
  threadId: this.codexThreadId,
@@ -1548,6 +1645,40 @@ var CodexThread = class {
1548
1645
  async abort(turnId) {
1549
1646
  await this.client.request("turn/interrupt", { threadId: this.codexThreadId, turnId });
1550
1647
  }
1648
+ async compact() {
1649
+ let startError;
1650
+ const startFailed = new Promise((resolve7) => {
1651
+ this.client.request("thread/compact/start", { threadId: this.codexThreadId }).then(void 0, (err) => {
1652
+ startError = err instanceof Error ? err : new Error(String(err));
1653
+ log.fail("agent", startError, { phase: "thread/compact/start" });
1654
+ resolve7("start-failed");
1655
+ });
1656
+ });
1657
+ let timer;
1658
+ const timeout = new Promise((resolve7) => {
1659
+ timer = setTimeout(() => resolve7("timeout"), COMPACT_TIMEOUT_MS);
1660
+ });
1661
+ const stream2 = this.client.stream()[Symbol.asyncIterator]();
1662
+ let compacted = false;
1663
+ let usage = null;
1664
+ try {
1665
+ while (true) {
1666
+ const step = await Promise.race([stream2.next(), startFailed, timeout]);
1667
+ if (step === "start-failed") throw startError ?? new Error("thread/compact/start \u8BF7\u6C42\u5931\u8D25");
1668
+ if (step === "timeout") throw new Error(`\u538B\u7F29\u8D85\u65F6\uFF08codex \u672A\u5728 ${COMPACT_TIMEOUT_MS / 1e3}s \u5185\u5B8C\u6210\uFF09`);
1669
+ if (step.done) break;
1670
+ const ev = mapNotification(step.value);
1671
+ if (!ev) continue;
1672
+ if (ev.type === "context_usage") usage = { usedTokens: ev.usedTokens, contextWindow: ev.contextWindow };
1673
+ else if (ev.type === "context_compacted") compacted = true;
1674
+ else if (ev.type === "error" && !ev.willRetry) throw new Error(ev.message);
1675
+ else if (ev.type === "done") break;
1676
+ }
1677
+ } finally {
1678
+ if (timer) clearTimeout(timer);
1679
+ }
1680
+ return { compacted, usage };
1681
+ }
1551
1682
  async close() {
1552
1683
  await this.client.close();
1553
1684
  }
@@ -1636,7 +1767,7 @@ var CodexAppServerBackend = class {
1636
1767
  }
1637
1768
  }
1638
1769
  async startThread(opts) {
1639
- const sandbox = sandboxParams(opts.mode, opts.network);
1770
+ const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
1640
1771
  const client = await this.spawn(opts.cwd);
1641
1772
  const res = await client.request("thread/start", {
1642
1773
  cwd: opts.cwd,
@@ -1648,7 +1779,7 @@ var CodexAppServerBackend = class {
1648
1779
  return new CodexThread(client, res.thread.id, opts.model, opts.effort);
1649
1780
  }
1650
1781
  async resumeThread(opts) {
1651
- const sandbox = sandboxParams(opts.mode, opts.network);
1782
+ const sandbox = withAutoCompact(sandboxParams(opts.mode, opts.network), opts.autoCompact);
1652
1783
  const client = await this.spawn(opts.cwd);
1653
1784
  const res = await client.request("thread/resume", {
1654
1785
  threadId: opts.codexThreadId,
@@ -1821,39 +1952,53 @@ function stampRenderToken(card2) {
1821
1952
  }
1822
1953
  }
1823
1954
  }
1824
- for (const k of Object.keys(obj)) visit(obj[k]);
1955
+ for (const k2 of Object.keys(obj)) visit(obj[k2]);
1825
1956
  };
1826
1957
  visit(card2);
1827
1958
  }
1959
+ function isCardIdNotReady(err) {
1960
+ const data = err?.response?.data;
1961
+ return data?.code === 230099 || /11310|cardid is invalid/i.test(data?.msg ?? "");
1962
+ }
1828
1963
  async function sendManagedCard(channel, to, card2, replyTo, replyInThread = false, receiveIdType = "chat_id") {
1829
1964
  stampRenderToken(card2);
1830
- const created = await channel.rawClient.cardkit.v1.card.create({
1831
- data: { type: "card_json", data: JSON.stringify(card2) }
1832
- });
1833
- const cardId = created.data?.card_id;
1834
- if (!cardId) {
1835
- throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
1836
- }
1837
- const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
1838
- let messageId;
1839
- if (replyTo) {
1840
- const sent = await channel.rawClient.im.v1.message.reply({
1841
- path: { message_id: replyTo },
1842
- data: { msg_type: "interactive", content, reply_in_thread: replyInThread }
1843
- });
1844
- messageId = sent.data?.message_id;
1845
- } else {
1846
- const sent = await channel.rawClient.im.v1.message.create({
1847
- params: { receive_id_type: receiveIdType },
1848
- data: { receive_id: to, msg_type: "interactive", content }
1849
- });
1850
- messageId = sent.data?.message_id;
1851
- }
1852
- if (!messageId) {
1853
- throw new Error("send card-by-reference returned no message_id");
1965
+ const data = JSON.stringify(card2);
1966
+ const attempt = async () => {
1967
+ const created = await channel.rawClient.cardkit.v1.card.create({ data: { type: "card_json", data } });
1968
+ const cardId = created.data?.card_id;
1969
+ if (!cardId) {
1970
+ throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
1971
+ }
1972
+ const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
1973
+ let messageId;
1974
+ if (replyTo) {
1975
+ const sent = await channel.rawClient.im.v1.message.reply({
1976
+ path: { message_id: replyTo },
1977
+ data: { msg_type: "interactive", content, reply_in_thread: replyInThread }
1978
+ });
1979
+ messageId = sent.data?.message_id;
1980
+ } else {
1981
+ const sent = await channel.rawClient.im.v1.message.create({
1982
+ params: { receive_id_type: receiveIdType },
1983
+ data: { receive_id: to, msg_type: "interactive", content }
1984
+ });
1985
+ messageId = sent.data?.message_id;
1986
+ }
1987
+ if (!messageId) {
1988
+ throw new Error("send card-by-reference returned no message_id");
1989
+ }
1990
+ byMessageId.set(messageId, { cardId, sequence: 0 });
1991
+ return { messageId, cardId };
1992
+ };
1993
+ for (let i = 0; ; i++) {
1994
+ try {
1995
+ return await attempt();
1996
+ } catch (err) {
1997
+ if (i >= 2 || !isCardIdNotReady(err)) throw err;
1998
+ log.fail("card", err, { phase: "managed-send", attempt: i, retry: true });
1999
+ await new Promise((r) => setTimeout(r, 400 * (i + 1)));
2000
+ }
1854
2001
  }
1855
- byMessageId.set(messageId, { cardId, sequence: 0 });
1856
- return { messageId, cardId };
1857
2002
  }
1858
2003
  async function updateManagedCard(channel, messageId, card2) {
1859
2004
  const entry = byMessageId.get(messageId);
@@ -1983,6 +2128,10 @@ function reduce(state, evt) {
1983
2128
  });
1984
2129
  return { ...state, blocks };
1985
2130
  }
2131
+ case "context_usage":
2132
+ return { ...state, usage: { used: evt.usedTokens, window: evt.contextWindow } };
2133
+ // context_compacted is surfaced as a standalone notice by the run loop, not
2134
+ // folded into the card — fall through to the no-op default.
1986
2135
  case "error":
1987
2136
  return { ...state, terminal: "error", errorMsg: evt.message, footer: null };
1988
2137
  case "done":
@@ -2102,6 +2251,9 @@ function image(imgKey, alt = "") {
2102
2251
  function note(content) {
2103
2252
  return { tag: "div", text: { tag: "lark_md", content, text_size: "notation", text_color: "grey" } };
2104
2253
  }
2254
+ function colorNote(content, color) {
2255
+ return { tag: "div", text: { tag: "lark_md", content, text_size: "notation", text_color: color } };
2256
+ }
2105
2257
  function hr() {
2106
2258
  return { tag: "hr" };
2107
2259
  }
@@ -2320,7 +2472,12 @@ function talkLine(noMention, tail) {
2320
2472
  function buildHelpCard(scope, noMention = true, isAdmin2 = false) {
2321
2473
  const elements = [];
2322
2474
  if (scope === "single") {
2323
- const lines = [talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406"), "\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6"];
2475
+ const lines = [
2476
+ talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406"),
2477
+ "\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6",
2478
+ "\xB7 `/context` \u2192 \u770B\u4E0A\u4E0B\u6587\u5360\u6BD4",
2479
+ "\xB7 `/compact` \u2192 \u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u91CA\u653E\u7A7A\u95F4\uFF09"
2480
+ ];
2324
2481
  if (isAdmin2) lines.push("\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09");
2325
2482
  lines.push("\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361");
2326
2483
  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")));
@@ -2331,6 +2488,8 @@ function buildHelpCard(scope, noMention = true, isAdmin2 = false) {
2331
2488
  md(
2332
2489
  `${talkLine(noMention, "\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD")}
2333
2490
  \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2491
+ \xB7 \`/context\` \u2192 \u770B\u4E0A\u4E0B\u6587\u5360\u6BD4
2492
+ \xB7 \`/compact\` \u2192 \u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u91CA\u653E\u7A7A\u95F4\uFF09
2334
2493
  \xB7 \`/help\` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361`
2335
2494
  ),
2336
2495
  note("\u5F00\u65B0\u8BDD\u9898\uFF1A\u56DE\u5230\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9\u3002")
@@ -2365,7 +2524,9 @@ function buildWelcomeCard(kind, docUrl, noMention = true) {
2365
2524
  "\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"
2366
2525
  ),
2367
2526
  md("\u{1F9F5} **\u8BDD\u9898\u5185**"),
2368
- 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"),
2527
+ md(
2528
+ "\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"
2529
+ ),
2369
2530
  note("\u4EFB\u610F\u573A\u666F\u53D1 `/help` \u770B\u5F53\u524D\u53EF\u7528\u547D\u4EE4\u3002")
2370
2531
  );
2371
2532
  }
@@ -2635,7 +2796,80 @@ function escapeInline2(s) {
2635
2796
  return s.replace(/\s+/g, " ").trim();
2636
2797
  }
2637
2798
 
2799
+ // src/card/context-gauge.ts
2800
+ var CTX_WARN = 0.7;
2801
+ var CTX_HIGH = 0.85;
2802
+ var CTX_CRIT = 0.95;
2803
+ function ctxTier(frac) {
2804
+ if (frac >= CTX_CRIT) return { level: 3, color: "red", dot: "\u{1F534}", advice: "\u5F3A\u70C8\u5EFA\u8BAE `/compact` \u538B\u7F29" };
2805
+ if (frac >= CTX_HIGH) return { level: 2, color: "orange", dot: "\u{1F7E0}", advice: "\u5EFA\u8BAE `/compact` \u538B\u7F29" };
2806
+ if (frac >= CTX_WARN) return { level: 1, color: "yellow", dot: "\u{1F7E1}", advice: "\u53EF\u8003\u8651 `/compact` \u538B\u7F29" };
2807
+ return { level: 0, color: "green", dot: "\u{1F7E2}", advice: "" };
2808
+ }
2809
+ function ctxPercent(used, window) {
2810
+ if (!window || window <= 0) return null;
2811
+ return Math.max(0, Math.min(100, Math.round(used / window * 100)));
2812
+ }
2813
+ function k(n) {
2814
+ return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(Math.max(0, Math.round(n)));
2815
+ }
2816
+ function runCardGauge(used, window) {
2817
+ const pct = ctxPercent(used, window);
2818
+ if (pct === null || !window) return null;
2819
+ const frac = used / window;
2820
+ if (frac < CTX_WARN) return null;
2821
+ const t = ctxTier(frac);
2822
+ return colorNote(`${t.dot} \u4E0A\u4E0B\u6587 ${pct}% \xB7 ${k(used)}/${k(window)} \xB7 ${t.advice}`, t.color);
2823
+ }
2824
+ function buildContextCard(used, window) {
2825
+ const pct = ctxPercent(used, window);
2826
+ if (pct === null) {
2827
+ 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";
2828
+ return card([note(line)], { summary: "\u4E0A\u4E0B\u6587\u7528\u91CF" });
2829
+ }
2830
+ const t = ctxTier(used / window);
2831
+ const els = [colorNote(`${t.dot} **\u4E0A\u4E0B\u6587 ${pct}%** \xB7 ${k(used)}/${k(window)} tokens`, t.color)];
2832
+ 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"));
2833
+ return card(els, { summary: "\u4E0A\u4E0B\u6587\u7528\u91CF" });
2834
+ }
2835
+ var COMPACT_SPINNER = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
2836
+ function buildCompactingCard(tick = 0) {
2837
+ const spin = COMPACT_SPINNER[(tick % COMPACT_SPINNER.length + COMPACT_SPINNER.length) % COMPACT_SPINNER.length];
2838
+ 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")], {
2839
+ summary: "\u6B63\u5728\u538B\u7F29\u4E0A\u4E0B\u6587"
2840
+ });
2841
+ }
2842
+ function buildCompactedCard(usage, before) {
2843
+ const els = [colorNote("\u2705 \u4E0A\u4E0B\u6587\u538B\u7F29\u5B8C\u6210", "green")];
2844
+ const pct = usage ? ctxPercent(usage.usedTokens, usage.contextWindow) : null;
2845
+ const dropped = usage != null && before != null && usage.usedTokens < before.used;
2846
+ if (usage && pct !== null && usage.contextWindow && (dropped || before == null)) {
2847
+ const beforePct = before ? ctxPercent(before.used, before.window) : null;
2848
+ const from = dropped && beforePct !== null ? `${beforePct}% \u2192 ` : "";
2849
+ 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`));
2850
+ } else {
2851
+ 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"));
2852
+ }
2853
+ return card(els, { summary: "\u4E0A\u4E0B\u6587\u538B\u7F29\u5B8C\u6210" });
2854
+ }
2855
+ function buildCompactFailedCard(message) {
2856
+ return card([colorNote(`\u26A0\uFE0F \u538B\u7F29\u5931\u8D25\uFF1A${message}`, "red")], { summary: "\u538B\u7F29\u5931\u8D25" });
2857
+ }
2858
+ function buildAutoCompactCard() {
2859
+ return card(
2860
+ [
2861
+ hr(),
2862
+ colorNote("\u{1F5DC}\uFE0F \u2500\u2500\u2500 \u4E0A\u4E0B\u6587\u5DF2\u81EA\u52A8\u538B\u7F29 \u2500\u2500\u2500", "blue"),
2863
+ 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")
2864
+ ],
2865
+ { summary: "\u4E0A\u4E0B\u6587\u5DF2\u81EA\u52A8\u538B\u7F29" }
2866
+ );
2867
+ }
2868
+
2638
2869
  // src/card/run-card.ts
2870
+ function gaugeEl(state) {
2871
+ return state.usage ? runCardGauge(state.usage.used, state.usage.window) : null;
2872
+ }
2639
2873
  var RC = {
2640
2874
  stop: "run.stop"
2641
2875
  };
@@ -2667,7 +2901,9 @@ function renderRunning(state, rc) {
2667
2901
  const answer = textParts.join("\n\n");
2668
2902
  if (answer) elements.push(mdStream(answer, ANSWER_EID));
2669
2903
  if (state.footer) elements.push(footerStatus(state.footer));
2670
- if (rc.cardKey) elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
2904
+ if (rc.cardKey && !rc.hideStop) elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
2905
+ const gauge = gaugeEl(state);
2906
+ if (gauge) elements.push(gauge);
2671
2907
  return elements;
2672
2908
  }
2673
2909
  function renderTerminal(state, rc) {
@@ -2701,6 +2937,8 @@ function renderTerminal(state, rc) {
2701
2937
  } else if (state.terminal === "done" && !answer) {
2702
2938
  elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
2703
2939
  }
2940
+ const gauge = gaugeEl(state);
2941
+ if (gauge) elements.push(gauge);
2704
2942
  return elements;
2705
2943
  }
2706
2944
  function lastTextIndex(blocks) {
@@ -2813,6 +3051,46 @@ function truncate4(s, n) {
2813
3051
  return s.length > n ? `${s.slice(0, n)}\u2026` : s;
2814
3052
  }
2815
3053
 
3054
+ // src/card/goal-card.ts
3055
+ function fmtTokens(n) {
3056
+ return Math.max(0, Math.round(n)).toLocaleString("en-US");
3057
+ }
3058
+ function fmtDuration(seconds) {
3059
+ const s = Math.max(0, Math.round(seconds));
3060
+ if (s < 60) return `\u7EA6 ${s} \u79D2`;
3061
+ const m = Math.floor(s / 60);
3062
+ const rem = s % 60;
3063
+ if (m < 60) return rem ? `\u7EA6 ${m} \u5206 ${rem} \u79D2` : `\u7EA6 ${m} \u5206`;
3064
+ const h = Math.floor(m / 60);
3065
+ const mm = m % 60;
3066
+ return mm ? `\u7EA6 ${h} \u65F6 ${mm} \u5206` : `\u7EA6 ${h} \u65F6`;
3067
+ }
3068
+ var ABNORMAL_REASON = {
3069
+ budgetLimited: "Token \u9884\u7B97\u7528\u5C3D",
3070
+ usageLimited: "\u8D26\u53F7\u7528\u91CF\u989D\u5EA6\u7528\u5C3D",
3071
+ blocked: "\u88AB\u963B\u585E\uFF0C\u9700\u4EBA\u5DE5\u4ECB\u5165",
3072
+ paused: "\u5DF2\u6682\u505C",
3073
+ timeout: "\u8FD0\u884C\u8D85\u8FC7\u65F6\u957F\u4E0A\u9650\u88AB\u4E2D\u6B62",
3074
+ error: "\u8FD0\u884C\u51FA\u9519"
3075
+ };
3076
+ function buildGoalDoneCard(d) {
3077
+ const ok = isGoalSuccess(d.status);
3078
+ const elements = [
3079
+ md(d.objective.trim() || "\uFF08\u65E0\u76EE\u6807\u63CF\u8FF0\uFF09"),
3080
+ hr(),
3081
+ note(`\u7528\u91CF\u3000${fmtTokens(d.tokensUsed)} tokens`),
3082
+ note(`\u8017\u65F6\u3000${fmtDuration(d.timeUsedSeconds)}`)
3083
+ ];
3084
+ if (!ok) {
3085
+ const reason = d.errorMessage?.trim() || ABNORMAL_REASON[d.status] || `\u72B6\u6001\uFF1A${d.status}`;
3086
+ elements.push(note(`\u539F\u56E0\u3000${reason}`));
3087
+ }
3088
+ return card(elements, {
3089
+ header: ok ? { title: "\u{1F3AF} \u76EE\u6807\u5DF2\u5B8C\u6210", template: "green" } : { title: "\u{1F3AF} \u76EE\u6807\u5DF2\u4E2D\u6B62", template: "orange" },
3090
+ summary: ok ? "\u76EE\u6807\u5DF2\u5B8C\u6210" : "\u76EE\u6807\u5DF2\u4E2D\u6B62"
3091
+ });
3092
+ }
3093
+
2816
3094
  // src/card/run-card-stream.ts
2817
3095
  var STREAM_THROTTLE_MS = 150;
2818
3096
  var RunCardStream = class {
@@ -2914,35 +3192,55 @@ var RunCardStream = class {
2914
3192
  }
2915
3193
  }
2916
3194
  /** Create the entity from the initial (running) card and send a message
2917
- * referencing it by card_id. Returns the carrier message id. */
3195
+ * referencing it by card_id. Returns the carrier message id.
3196
+ *
3197
+ * A just-created CardKit entity occasionally hasn't propagated when the message
3198
+ * referencing it is sent — Feishu 400s with 230099 / ErrCode 11310 "cardid is
3199
+ * invalid" and the run card silently fails to appear (this surfaced as
3200
+ * intermittent intake.fail). Same transient, same fix as
3201
+ * {@link ../card/managed#sendManagedCard}: retry the whole create+send with a
3202
+ * short backoff. Only this transient retries — Feishu rejected the message
3203
+ * outright (nothing sent), and a re-created entity that's never referenced is a
3204
+ * harmless orphan, so no duplicate card. */
2918
3205
  async create(channel, chatId, initialCard, opts) {
2919
- const created = await channel.rawClient.cardkit.v1.card.create({
2920
- data: { type: "card_json", data: JSON.stringify(initialCard) }
2921
- });
2922
- const cardId = created.data?.card_id;
2923
- if (!cardId) {
2924
- throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
2925
- }
2926
- this.cardId = cardId;
2927
- this.lastContent = JSON.stringify(initialCard);
2928
- const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
2929
- let messageId;
2930
- if (opts.replyTo) {
2931
- const r = await channel.rawClient.im.v1.message.reply({
2932
- path: { message_id: opts.replyTo },
2933
- data: { msg_type: "interactive", content, reply_in_thread: opts.replyInThread ?? false }
2934
- });
2935
- messageId = r.data?.message_id;
2936
- } else {
2937
- const r = await channel.rawClient.im.v1.message.create({
2938
- params: { receive_id_type: "chat_id" },
2939
- data: { receive_id: chatId, msg_type: "interactive", content }
3206
+ const attempt = async () => {
3207
+ const created = await channel.rawClient.cardkit.v1.card.create({
3208
+ data: { type: "card_json", data: JSON.stringify(initialCard) }
2940
3209
  });
2941
- messageId = r.data?.message_id;
3210
+ const cardId = created.data?.card_id;
3211
+ if (!cardId) {
3212
+ throw new Error(`cardkit.card.create returned no card_id: ${JSON.stringify(created).slice(0, 200)}`);
3213
+ }
3214
+ this.cardId = cardId;
3215
+ this.lastContent = JSON.stringify(initialCard);
3216
+ const content = JSON.stringify({ type: "card", data: { card_id: cardId } });
3217
+ let messageId;
3218
+ if (opts.replyTo) {
3219
+ const r = await channel.rawClient.im.v1.message.reply({
3220
+ path: { message_id: opts.replyTo },
3221
+ data: { msg_type: "interactive", content, reply_in_thread: opts.replyInThread ?? false }
3222
+ });
3223
+ messageId = r.data?.message_id;
3224
+ } else {
3225
+ const r = await channel.rawClient.im.v1.message.create({
3226
+ params: { receive_id_type: "chat_id" },
3227
+ data: { receive_id: chatId, msg_type: "interactive", content }
3228
+ });
3229
+ messageId = r.data?.message_id;
3230
+ }
3231
+ if (!messageId) throw new Error("run card send returned no message_id");
3232
+ this._messageId = messageId;
3233
+ return messageId;
3234
+ };
3235
+ for (let i = 0; ; i++) {
3236
+ try {
3237
+ return await attempt();
3238
+ } catch (err) {
3239
+ if (i >= 2 || !isCardIdNotReady(err)) throw err;
3240
+ log.fail("card", err, { phase: "run-stream-create", attempt: i, retry: true });
3241
+ await new Promise((r) => setTimeout(r, 400 * (i + 1)));
3242
+ }
2942
3243
  }
2943
- if (!messageId) throw new Error("run card send returned no message_id");
2944
- this._messageId = messageId;
2945
- return messageId;
2946
3244
  }
2947
3245
  /** Throttled whole-card stream update. Skips identical/too-soon pushes;
2948
3246
  * `force` flushes regardless (still de-duped on content). */
@@ -3226,8 +3524,8 @@ async function updateProject(name, patch) {
3226
3524
  if (!p) return;
3227
3525
  const actual = typeof patch === "function" ? patch(p) : patch;
3228
3526
  const target = p;
3229
- for (const [k, v] of Object.entries(actual)) {
3230
- if (v !== void 0) target[k] = v;
3527
+ for (const [k2, v] of Object.entries(actual)) {
3528
+ if (v !== void 0) target[k2] = v;
3231
3529
  }
3232
3530
  await write(projects);
3233
3531
  });
@@ -3286,13 +3584,18 @@ var DM = {
3286
3584
  rmAllowed: "dm.allow.rm",
3287
3585
  // 项目设置容器(项目列表 / 建项目完成卡 进入),以后的项目级设置项往这里加
3288
3586
  projectSettings: "dm.projectSettings",
3587
+ // 🧵 话题钻取:项目总览的「🧵 N 话题」按钮 → 该项目话题列表卡
3588
+ projectTopics: "dm.projectTopics",
3289
3589
  setNoMentionDm: "dm.proj.noMention",
3590
+ // 🗜️ 自动压缩:项目级开关(同群设置里的那个,DM 里也能改),按钮携带项目名 n
3591
+ setAutoCompactDm: "dm.proj.autoCompact",
3290
3592
  // 🔐 权限:codex 沙箱档位(管理员档 + 普通用户档)+ 联网,做成下拉表单(选+提交)
3291
3593
  permission: "dm.proj.perm",
3292
3594
  permissionSubmit: "dm.proj.perm.submit"
3293
3595
  };
3294
3596
  var GS = {
3295
- setNoMention: "gs.noMention"
3597
+ setNoMention: "gs.noMention",
3598
+ setAutoCompact: "gs.autoCompact"
3296
3599
  };
3297
3600
  function kindLabel(kind) {
3298
3601
  return kind === "single" ? "\u{1F4AC} \u5355\u4F1A\u8BDD\u7FA4" : "\u{1F465} \u591A\u8BDD\u9898\u7FA4";
@@ -3565,6 +3868,7 @@ function buildNewProjectDoneCard(p) {
3565
3868
  );
3566
3869
  return card(elements, { header: { title, template: "green" } });
3567
3870
  }
3871
+ var PROJECT_TOPICS_MAX = 50;
3568
3872
  function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map()) {
3569
3873
  if (projects.length === 0) {
3570
3874
  return card(
@@ -3574,25 +3878,15 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3574
3878
  }
3575
3879
  const elements = [];
3576
3880
  for (const p of projects) {
3881
+ const topicCount = (p.chatId ? sessionsByChat.get(p.chatId) : void 0)?.length ?? 0;
3882
+ const dir = `\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`;
3883
+ 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";
3577
3884
  elements.push(md(`**${p.name}**${p.blank ? " _(\u7A7A\u767D)_" : ""}`));
3578
- elements.push(note(`\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`));
3579
- elements.push(
3580
- note(
3581
- 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"
3582
- )
3583
- );
3584
- const sessions = (p.chatId ? sessionsByChat.get(p.chatId) : void 0) ?? [];
3585
- if (sessions.length === 0) {
3586
- elements.push(note("\uFF08\u6682\u65E0\u8BDD\u9898\uFF09"));
3587
- } else {
3588
- const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
3589
- for (const s of sorted) {
3590
- const title = (s.summary || "(\u7A7A)").replace(/\s+/g, " ").slice(0, 40);
3591
- elements.push(note(`\xB7 ${title} \xB7 ${relativeTime(s.updatedAt)}`));
3592
- }
3593
- }
3885
+ elements.push(note(`${dir}
3886
+ ${meta}`));
3594
3887
  const row = [];
3595
3888
  if (p.chatId) row.push(linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId)));
3889
+ row.push(button(`\u{1F9F5} ${topicCount} \u8BDD\u9898`, { a: DM.projectTopics, n: p.name }));
3596
3890
  row.push(button("\u2699\uFE0F \u8BBE\u7F6E", { a: DM.projectSettings, n: p.name }));
3597
3891
  row.push(button("\u{1F5D1} \u5220\u9664", { a: DM.rmConfirm, n: p.name }, "danger"));
3598
3892
  elements.push(actions(row));
@@ -3602,6 +3896,26 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3602
3896
  elements.push(actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })]));
3603
3897
  return card(elements, { header: { title: "\u{1F4C1} \u9879\u76EE\u5217\u8868", template: "wathet" } });
3604
3898
  }
3899
+ function buildProjectTopicsCard(project, sessions) {
3900
+ const elements = [md(`**${project.name}** \xB7 \u5171 ${sessions.length} \u4E2A\u8BDD\u9898`)];
3901
+ if (sessions.length === 0) {
3902
+ elements.push(note("\uFF08\u6682\u65E0\u8BDD\u9898\uFF09"));
3903
+ } else {
3904
+ const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
3905
+ for (const s of sorted.slice(0, PROJECT_TOPICS_MAX)) {
3906
+ const title = (s.summary || "(\u7A7A)").replace(/\s+/g, " ").slice(0, 50);
3907
+ elements.push(note(`\xB7 ${title} \xB7 ${relativeTime(s.updatedAt)}`));
3908
+ }
3909
+ if (sorted.length > PROJECT_TOPICS_MAX) {
3910
+ 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`));
3911
+ }
3912
+ }
3913
+ const nav = [];
3914
+ if (project.chatId) nav.push(linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(project.chatId)));
3915
+ nav.push(button("\u2B05\uFE0F \u9879\u76EE\u5217\u8868", { a: DM.projects }));
3916
+ elements.push(hr(), actions(nav));
3917
+ return card(elements, { header: { title: `\u{1F9F5} \u8BDD\u9898 \xB7 ${project.name}`, template: "wathet" } });
3918
+ }
3605
3919
  function buildRmConfirmCard(name, origin) {
3606
3920
  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";
3607
3921
  return card(
@@ -3675,6 +3989,7 @@ function buildWatchdogCustomCard(cfg) {
3675
3989
  function buildGroupSettingsCard(project) {
3676
3990
  const kind = project.kind ?? "multi";
3677
3991
  const noMention = project.noMention ?? defaultNoMention(project);
3992
+ const autoCompact = project.autoCompact ?? true;
3678
3993
  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";
3679
3994
  return card(
3680
3995
  [
@@ -3685,7 +4000,12 @@ function buildGroupSettingsCard(project) {
3685
4000
  { label: "\u5173", value: "off" }
3686
4001
  ]),
3687
4002
  note(scopeNote),
3688
- 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")
4003
+ 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"),
4004
+ ...optionRow("\u{1F5DC}\uFE0F \u81EA\u52A8\u538B\u7F29\u4E0A\u4E0B\u6587", GS.setAutoCompact, autoCompact ? "on" : "off", [
4005
+ { label: "\u5F00", value: "on" },
4006
+ { label: "\u5173", value: "off" }
4007
+ ]),
4008
+ 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")
3689
4009
  ],
3690
4010
  { header: { title: "\u2699\uFE0F \u7FA4\u8BBE\u7F6E", template: "blue" } }
3691
4011
  );
@@ -3803,6 +4123,7 @@ function buildPermissionCard(p) {
3803
4123
  function buildProjectSettingsCard(project) {
3804
4124
  const kind = project.kind ?? "multi";
3805
4125
  const noMention = project.noMention ?? defaultNoMention(project);
4126
+ const autoCompact = project.autoCompact ?? true;
3806
4127
  return card(
3807
4128
  [
3808
4129
  md(`**\u9879\u76EE\u8BBE\u7F6E** \xB7 ${project.name}`),
@@ -3820,6 +4141,13 @@ function buildProjectSettingsCard(project) {
3820
4141
  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"
3821
4142
  ),
3822
4143
  hr(),
4144
+ md("\u{1F5DC}\uFE0F \u81EA\u52A8\u538B\u7F29\u4E0A\u4E0B\u6587"),
4145
+ actions([
4146
+ button("\u5F00", { a: DM.setAutoCompactDm, v: "on", n: project.name }, autoCompact ? "primary" : "default"),
4147
+ button("\u5173", { a: DM.setAutoCompactDm, v: "off", n: project.name }, autoCompact ? "default" : "primary")
4148
+ ]),
4149
+ 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"),
4150
+ hr(),
3823
4151
  actions([button("\u{1F6E1} \u54CD\u5E94\u767D\u540D\u5355", { a: DM.allowlist, n: project.name }, "primary")]),
3824
4152
  note("\u8BBE\u7F6E\u8C01\u80FD\u8BA9\u6211\u5728\u672C\u7FA4\u54CD\u5E94 / \u8DD1 codex\uFF08\u7A7A = \u6240\u6709\u4EBA\uFF09\u3002"),
3825
4153
  hr(),
@@ -5365,8 +5693,8 @@ async function patchSession(threadId, patch) {
5365
5693
  const rec = sessions.find((s) => s.threadId === threadId);
5366
5694
  if (!rec) return;
5367
5695
  const target = rec;
5368
- for (const [k, v] of Object.entries(patch)) {
5369
- if (v !== void 0) target[k] = v;
5696
+ for (const [k2, v] of Object.entries(patch)) {
5697
+ if (v !== void 0) target[k2] = v;
5370
5698
  }
5371
5699
  rec.updatedAt = Date.now();
5372
5700
  await write2(sessions);
@@ -5484,7 +5812,7 @@ function walkForImageKeys(node, out) {
5484
5812
  }
5485
5813
  const obj = node;
5486
5814
  if (obj.tag === "img" && typeof obj.image_key === "string") out.push(obj.image_key);
5487
- for (const k of Object.keys(obj)) walkForImageKeys(obj[k], out);
5815
+ for (const k2 of Object.keys(obj)) walkForImageKeys(obj[k2], out);
5488
5816
  }
5489
5817
  async function downloadOne(channel, ref, index) {
5490
5818
  try {
@@ -6112,6 +6440,11 @@ function selectValue(formValue, name) {
6112
6440
  function asTier(v) {
6113
6441
  return v === "qa" || v === "write" || v === "full" ? v : void 0;
6114
6442
  }
6443
+ function parseGoalTrigger(text) {
6444
+ if (!/(^|\s)\/goal(?=\s|$)/i.test(text)) return null;
6445
+ const objective = text.replace(/(^|\s)\/goal(?=\s|$)/gi, " ").replace(/\s+/g, " ").trim();
6446
+ return objective.length > 0 ? objective : null;
6447
+ }
6115
6448
  function createOrchestrator(channel, cfg, fallbackCwd) {
6116
6449
  const backend = createBackend();
6117
6450
  const sessions = /* @__PURE__ */ new Map();
@@ -6125,6 +6458,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6125
6458
  const runCards = /* @__PURE__ */ new Map();
6126
6459
  const runStreams = /* @__PURE__ */ new Map();
6127
6460
  const lastRunCard = /* @__PURE__ */ new Map();
6461
+ const lastUsage = /* @__PURE__ */ new Map();
6128
6462
  let modelsCache = null;
6129
6463
  async function listModels() {
6130
6464
  if (!modelsCache) modelsCache = await backend.listModels();
@@ -6206,6 +6540,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6206
6540
  }
6207
6541
  const text = msg.content.trim();
6208
6542
  const cmd = parseCommand(text);
6543
+ const goalObjective = parseGoalTrigger(text);
6209
6544
  if ((project?.kind ?? "multi") === "single") {
6210
6545
  if (cmd === "help") {
6211
6546
  await postHelpCard(msg, "single", false, project);
@@ -6220,6 +6555,19 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6220
6555
  await postModelCard(msg, ts.sessionKey);
6221
6556
  return;
6222
6557
  }
6558
+ if (cmd === "compact") {
6559
+ await runCompact(msg, ts.sessionKey, false, ts);
6560
+ return;
6561
+ }
6562
+ if (cmd === "context") {
6563
+ await postContextCard(msg, ts.sessionKey, false);
6564
+ return;
6565
+ }
6566
+ if (goalObjective) {
6567
+ void addReaction(msg.messageId, "OKR");
6568
+ startReservedRun(msg, goalObjective, ts.sessionKey, true, project, ts, void 0, void 0, void 0, true);
6569
+ return;
6570
+ }
6223
6571
  handleTurn(msg, text, ts.sessionKey, true, project, ts);
6224
6572
  return;
6225
6573
  }
@@ -6233,6 +6581,19 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6233
6581
  await postModelCard(msg, ts.sessionKey);
6234
6582
  return;
6235
6583
  }
6584
+ if (cmd === "compact") {
6585
+ await runCompact(msg, ts.sessionKey, true, ts);
6586
+ return;
6587
+ }
6588
+ if (cmd === "context") {
6589
+ await postContextCard(msg, ts.sessionKey, true);
6590
+ return;
6591
+ }
6592
+ if (goalObjective) {
6593
+ void addReaction(msg.messageId, "OKR");
6594
+ startReservedRun(msg, goalObjective, ts.sessionKey, false, project, ts, void 0, void 0, void 0, true);
6595
+ return;
6596
+ }
6236
6597
  handleTurn(msg, text, ts.sessionKey, false, project, ts);
6237
6598
  return;
6238
6599
  }
@@ -6248,8 +6609,13 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6248
6609
  await postGroupSettings(msg, project);
6249
6610
  return;
6250
6611
  }
6251
- if (cmd === "model") {
6252
- 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);
6612
+ if (cmd === "model" || cmd === "compact" || cmd === "context") {
6613
+ 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);
6614
+ return;
6615
+ }
6616
+ if (goalObjective) {
6617
+ void addReaction(msg.messageId, "OKR");
6618
+ startTopicDirectly(msg, goalObjective, project, true);
6253
6619
  return;
6254
6620
  }
6255
6621
  startTopicDirectly(msg, text, project);
@@ -6257,13 +6623,14 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6257
6623
  function parseCommand(text) {
6258
6624
  const m = /^\/(\w+)/.exec(text);
6259
6625
  const name = m?.[1]?.toLowerCase();
6260
- return name === "resume" || name === "model" || name === "settings" || name === "help" ? name : null;
6626
+ return name === "resume" || name === "model" || name === "settings" || name === "help" || name === "compact" || name === "context" ? name : null;
6261
6627
  }
6262
6628
  function shouldRespondWithoutMention(project, msg) {
6263
6629
  if (!(project.noMention ?? defaultNoMention(project))) return false;
6264
6630
  if (msg.mentionAll || msg.mentions.some((m) => !m.isBot)) return false;
6265
6631
  if ((project.kind ?? "multi") === "single") return true;
6266
- return Boolean(msg.threadId) || parseCommand(msg.content.trim()) !== null;
6632
+ const content = msg.content.trim();
6633
+ return Boolean(msg.threadId) || parseCommand(content) !== null || parseGoalTrigger(content) !== null;
6267
6634
  }
6268
6635
  async function denyAdminCommand(msg, cmd) {
6269
6636
  await channel.send(msg.chatId, { markdown: `\u26A0\uFE0F \`/${cmd}\` \u4EC5 bot \u7BA1\u7406\u5458\u53EF\u7528\u3002` }, { replyTo: msg.messageId }).catch(() => void 0);
@@ -6286,7 +6653,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6286
6653
  function turnPerm(project, senderId) {
6287
6654
  if (!project) return {};
6288
6655
  const t = turnTier(project, isAdmin(cfg, senderId));
6289
- return { mode: t.mode, network: project.network, roleSuffix: t.split ? t.role : void 0 };
6656
+ return { mode: t.mode, network: project.network, autoCompact: project.autoCompact, roleSuffix: t.split ? t.role : void 0 };
6290
6657
  }
6291
6658
  function turnSession(baseKey, project, senderId) {
6292
6659
  const perm = turnPerm(project, senderId);
@@ -6335,9 +6702,13 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6335
6702
  }
6336
6703
  startReservedRun(msg, text, sessionKey, flat, project, perm);
6337
6704
  }
6338
- function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages, preIngested, summaryText2) {
6705
+ function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages, preIngested, summaryText2, goal) {
6339
6706
  const existing = active.get(sessionKey);
6340
6707
  if (existing) {
6708
+ if (goal) {
6709
+ void channel.send(msg.chatId, { markdown: "\u5F53\u524D\u4F1A\u8BDD\u6709\u4EFB\u52A1\u5728\u8DD1\uFF0C\u8BF7\u7B49\u5B83\u7ED3\u675F\u540E\u518D\u53D1 `/goal`\u3002" }, { replyTo: msg.messageId, replyInThread: !flat }).catch(() => void 0);
6710
+ return;
6711
+ }
6341
6712
  existing.queue.push({ text, images: preloadedImages });
6342
6713
  log.info("intake", "queued", { depth: existing.queue.length });
6343
6714
  return;
@@ -6345,13 +6716,14 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6345
6716
  const reserved = { queue: [], requesterOpenId: msg.senderId };
6346
6717
  active.set(sessionKey, reserved);
6347
6718
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
6348
- const reaction = runReaction(msg.messageId, !sema.hasFree());
6719
+ const reaction = goal ? void 0 : runReaction(msg.messageId, !sema.hasFree());
6349
6720
  try {
6350
6721
  const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
6351
6722
  let firstText = preIngested ? text : await ingestContext(msg, text);
6352
6723
  const { thread: resolved, recreated } = await resolveThread(sessionKey, msg.chatId, {
6353
6724
  mode: perm.mode,
6354
- network: perm.network
6725
+ network: perm.network,
6726
+ autoCompact: perm.autoCompact
6355
6727
  });
6356
6728
  let thread = resolved;
6357
6729
  const neverSeen = !thread;
@@ -6359,7 +6731,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6359
6731
  const prior = neverSeen ? void 0 : await getSession(sessionKey);
6360
6732
  if (!thread) {
6361
6733
  const cwd = project?.cwd ?? fallbackCwd;
6362
- thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network });
6734
+ thread = await backend.startThread({ cwd, mode: perm.mode, network: perm.network, autoCompact: perm.autoCompact });
6363
6735
  sessions.set(sessionKey, thread);
6364
6736
  await upsertSession({
6365
6737
  threadId: sessionKey,
@@ -6375,7 +6747,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6375
6747
  updatedAt: Date.now()
6376
6748
  });
6377
6749
  }
6378
- if (msg.threadId && (codexEmpty || prior?.lastSeenAt !== void 0)) {
6750
+ if (!goal && msg.threadId && (codexEmpty || prior?.lastSeenAt !== void 0)) {
6379
6751
  const history = await fetchThreadContext(channel, msg.threadId, {
6380
6752
  sinceTime: codexEmpty ? 0 : prior?.lastSeenAt ?? 0,
6381
6753
  excludeMessageId: msg.messageId
@@ -6384,23 +6756,22 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6384
6756
  }
6385
6757
  if (!neverSeen) void patchSession(sessionKey, { lastSeenAt: msg.createTime }).catch(() => void 0);
6386
6758
  reserved.thread = thread;
6387
- await launchRun(
6388
- {
6389
- chatId: msg.chatId,
6390
- replyTo: msg.messageId,
6391
- replyInThread: !flat,
6392
- flat,
6393
- thread,
6394
- firstText,
6395
- images,
6396
- knownThreadId: sessionKey,
6397
- requesterOpenId: msg.senderId
6398
- },
6399
- reaction
6400
- );
6759
+ const launchOpts = {
6760
+ chatId: msg.chatId,
6761
+ replyTo: msg.messageId,
6762
+ replyInThread: !flat,
6763
+ flat,
6764
+ thread,
6765
+ firstText,
6766
+ images,
6767
+ knownThreadId: sessionKey,
6768
+ requesterOpenId: msg.senderId
6769
+ };
6770
+ if (goal) await launchGoalRun(launchOpts);
6771
+ else await launchRun(launchOpts, reaction);
6401
6772
  } catch (err) {
6402
6773
  active.delete(sessionKey);
6403
- reaction.done();
6774
+ reaction?.done();
6404
6775
  log.fail("intake", err);
6405
6776
  await channel.send(msg.chatId, { markdown: `\u274C ${err instanceof Error ? err.message : String(err)}` }, { replyTo: msg.messageId, replyInThread: !flat }).catch(() => void 0);
6406
6777
  }
@@ -6418,7 +6789,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6418
6789
  model: rec.model,
6419
6790
  effort: rec.effort,
6420
6791
  mode: perm?.mode,
6421
- network: perm?.network
6792
+ network: perm?.network,
6793
+ autoCompact: perm?.autoCompact
6422
6794
  });
6423
6795
  sessions.set(threadId, resumed);
6424
6796
  return { thread: resumed, recreated: false };
@@ -6431,7 +6803,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6431
6803
  model: rec.model,
6432
6804
  effort: rec.effort,
6433
6805
  mode: perm?.mode ?? project?.mode,
6434
- network: perm?.network ?? project?.network
6806
+ network: perm?.network ?? project?.network,
6807
+ autoCompact: perm?.autoCompact ?? project?.autoCompact
6435
6808
  });
6436
6809
  sessions.set(threadId, fresh);
6437
6810
  await patchSession(threadId, { codexThreadId: fresh.codexThreadId }).catch(() => void 0);
@@ -6450,44 +6823,47 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6450
6823
  }
6451
6824
  if (closed) log.info("console", "tier-evict", { chatId, closed });
6452
6825
  }
6453
- function startTopicDirectly(msg, text, project) {
6826
+ function startTopicDirectly(msg, text, project, goal) {
6454
6827
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
6455
- const reaction = runReaction(msg.messageId, !sema.hasFree());
6828
+ const reaction = goal ? void 0 : runReaction(msg.messageId, !sema.hasFree());
6456
6829
  const cwd = project?.cwd ?? fallbackCwd;
6457
6830
  const perm = turnPerm(project, msg.senderId);
6458
6831
  if (project) void refreshBranch(channel, project).catch(() => void 0);
6459
6832
  const { model, effort } = pickDefault(await listModels());
6460
6833
  let thread;
6461
6834
  try {
6462
- thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network });
6835
+ thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network, autoCompact: perm.autoCompact });
6463
6836
  } catch (err) {
6464
- reaction.done();
6837
+ reaction?.done();
6465
6838
  log.fail("card", err, { phase: "start-topic" });
6466
6839
  await channel.send(msg.chatId, { markdown: `\u274C \u542F\u52A8\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}` }, { replyTo: msg.messageId }).catch(() => void 0);
6467
6840
  return;
6468
6841
  }
6469
6842
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
6470
6843
  const firstText = await ingestContext(msg, text) || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
6471
- log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0 });
6472
- await launchRun(
6473
- {
6474
- chatId: msg.chatId,
6475
- replyTo: msg.messageId,
6476
- replyInThread: true,
6477
- thread,
6478
- firstText,
6479
- images,
6480
- model,
6481
- effort,
6482
- cwd,
6483
- summary: stripFileTokens(text).slice(0, 80) || "(\u7A7A)",
6484
- requesterOpenId: msg.senderId,
6485
- roleSuffix: perm.roleSuffix
6486
- },
6487
- reaction,
6488
- () => reaction.done()
6489
- // topic created → ✅ DONE (don't wait for the reply)
6490
- );
6844
+ log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0, goal: Boolean(goal) });
6845
+ const launchOpts = {
6846
+ chatId: msg.chatId,
6847
+ replyTo: msg.messageId,
6848
+ replyInThread: true,
6849
+ thread,
6850
+ firstText,
6851
+ images,
6852
+ model,
6853
+ effort,
6854
+ cwd,
6855
+ summary: stripFileTokens(text).slice(0, 80) || "(\u7A7A)",
6856
+ requesterOpenId: msg.senderId,
6857
+ roleSuffix: perm.roleSuffix
6858
+ };
6859
+ if (goal) await launchGoalRun(launchOpts);
6860
+ else
6861
+ await launchRun(
6862
+ launchOpts,
6863
+ reaction,
6864
+ () => reaction?.done()
6865
+ // topic created → ✅ DONE (don't wait for the reply)
6866
+ );
6491
6867
  }).catch((err) => log.fail("intake", err));
6492
6868
  }
6493
6869
  async function postResumeCard(msg) {
@@ -6533,6 +6909,76 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6533
6909
  log.info("card", "model", { threadId: sessionKey, model: state.model, effort: state.effort });
6534
6910
  });
6535
6911
  }
6912
+ async function postContextCard(msg, sessionKey, inThread) {
6913
+ const u = lastUsage.get(sessionKey);
6914
+ await sendManagedCard(channel, msg.chatId, buildContextCard(u?.used ?? 0, u?.window ?? null), msg.messageId, inThread).catch(
6915
+ (err) => log.fail("card", err, { phase: "context" })
6916
+ );
6917
+ }
6918
+ const COMPACT_ANIM_INTERVAL_MS = 800;
6919
+ async function runCompact(msg, sessionKey, inThread, perm) {
6920
+ const reply = (markdown) => channel.send(msg.chatId, { markdown }, { replyTo: msg.messageId, replyInThread: inThread }).then(() => void 0, () => void 0);
6921
+ if (active.get(sessionKey)) {
6922
+ await reply("\u23F3 \u8FD9\u4E00\u8F6E\u8FD8\u5728\u8DD1\uFF0C\u7ED3\u675F\u540E\u518D `/compact`\u3002");
6923
+ return;
6924
+ }
6925
+ const { thread } = await resolveThread(sessionKey, msg.chatId, {
6926
+ mode: perm.mode,
6927
+ network: perm.network,
6928
+ autoCompact: perm.autoCompact
6929
+ });
6930
+ if (!thread) {
6931
+ await reply("\u8FD9\u4E2A\u4F1A\u8BDD\u8FD8\u6CA1\u5F00\u59CB\uFF0C\u5148\u53D1\u6761\u6D88\u606F\u804A\u4E24\u53E5\u518D `/compact`\u3002");
6932
+ return;
6933
+ }
6934
+ let cardMsgId;
6935
+ try {
6936
+ const sent = await sendManagedCard(channel, msg.chatId, buildCompactingCard(0), msg.messageId, inThread);
6937
+ cardMsgId = sent.messageId;
6938
+ } catch (err) {
6939
+ log.fail("card", err, { phase: "compact-start-card" });
6940
+ }
6941
+ let stop = false;
6942
+ const wakers = [];
6943
+ const sleep = (ms) => new Promise((res) => {
6944
+ const t = setTimeout(res, ms);
6945
+ wakers.push(() => {
6946
+ clearTimeout(t);
6947
+ res();
6948
+ });
6949
+ });
6950
+ const anim = (async () => {
6951
+ let tick = 0;
6952
+ while (!stop && cardMsgId) {
6953
+ await sleep(COMPACT_ANIM_INTERVAL_MS);
6954
+ if (stop || !cardMsgId) break;
6955
+ tick++;
6956
+ await updateManagedCard(channel, cardMsgId, buildCompactingCard(tick)).catch(() => void 0);
6957
+ }
6958
+ })();
6959
+ const settle = async (result) => {
6960
+ stop = true;
6961
+ wakers.forEach((w) => w());
6962
+ await anim;
6963
+ if (cardMsgId && await updateManagedCard(channel, cardMsgId, result)) return;
6964
+ await sendManagedCard(channel, msg.chatId, result, msg.messageId, inThread).catch(
6965
+ (err) => log.fail("card", err, { phase: "compact-settle" })
6966
+ );
6967
+ };
6968
+ const before = lastUsage.get(sessionKey) ?? null;
6969
+ try {
6970
+ const { usage } = await thread.compact();
6971
+ if (usage) lastUsage.set(sessionKey, { used: usage.usedTokens, window: usage.contextWindow });
6972
+ else lastUsage.delete(sessionKey);
6973
+ log.info("intake", "compact", { sessionKey, used: usage?.usedTokens ?? null, before: before?.used ?? null });
6974
+ await settle(buildCompactedCard(usage, before));
6975
+ } catch (err) {
6976
+ const m = err instanceof Error ? err.message : String(err);
6977
+ const unsupported = /method not found|-32601|unknown (method|request)/i.test(m);
6978
+ log.fail("intake", err, { phase: "compact" });
6979
+ await settle(buildCompactFailedCard(unsupported ? "\u5F53\u524D codex \u7248\u672C\u4E0D\u652F\u6301 /compact\uFF0C\u8BF7\u5347\u7EA7\u540E\u518D\u8BD5\u3002" : m));
6980
+ }
6981
+ }
6536
6982
  async function postHelpCard(msg, scope, inThread = false, project) {
6537
6983
  const noMention = project ? project.noMention ?? defaultNoMention(project) : true;
6538
6984
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
@@ -6544,28 +6990,31 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6544
6990
  }
6545
6991
  const dispatcher = new CardDispatcher(channel, cfg);
6546
6992
  const PENDING_TTL_MS = 30 * 6e4;
6993
+ const GOAL_MAX_MS = 30 * 6e4;
6547
6994
  const CARD_SETTLE_MS = 500;
6548
6995
  const settleUpdate = (msgId, c, fallbackChatId) => {
6549
6996
  const armedAt = Date.now();
6550
6997
  void (async () => {
6551
- await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
6552
- const card2 = typeof c === "function" ? await c() : c;
6553
- const ok = await updateManagedCard(channel, msgId, card2);
6554
- log.info("console", "settle-update", { msgId, ok, waitedMs: Date.now() - armedAt, fallback: !ok && !!fallbackChatId });
6555
- if (!ok && fallbackChatId) {
6556
- await sendManagedCard(channel, fallbackChatId, card2).catch(
6557
- (err) => log.fail("console", err, { phase: "settle-fallback" })
6558
- );
6998
+ try {
6999
+ await new Promise((r) => setTimeout(r, CARD_SETTLE_MS));
7000
+ const card2 = typeof c === "function" ? await c() : c;
7001
+ const ok = await updateManagedCard(channel, msgId, card2);
7002
+ log.info("console", "settle-update", { msgId, ok, waitedMs: Date.now() - armedAt, fallback: !ok && !!fallbackChatId });
7003
+ if (!ok && fallbackChatId) {
7004
+ await sendManagedCard(channel, fallbackChatId, card2);
7005
+ }
7006
+ } catch (err) {
7007
+ log.fail("console", err, { phase: "settle-update", msgId });
6559
7008
  }
6560
7009
  })();
6561
7010
  };
6562
7011
  function pruneResumePending() {
6563
7012
  const now = Date.now();
6564
- for (const [k, s] of resumePending) if (now - s.createdAt > PENDING_TTL_MS) resumePending.delete(k);
7013
+ for (const [k2, s] of resumePending) if (now - s.createdAt > PENDING_TTL_MS) resumePending.delete(k2);
6565
7014
  }
6566
7015
  function pruneModelPending() {
6567
7016
  const now = Date.now();
6568
- for (const [k, s] of modelPending) if (now - s.createdAt > PENDING_TTL_MS) modelPending.delete(k);
7017
+ for (const [k2, s] of modelPending) if (now - s.createdAt > PENDING_TTL_MS) modelPending.delete(k2);
6569
7018
  }
6570
7019
  function authPending(map, evt) {
6571
7020
  const state = map.get(evt.messageId);
@@ -6929,6 +7378,19 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
6929
7378
  }
6930
7379
  return buildGroupSettingsCard({ name: "\u672C\u7FA4", kind: "multi", noMention: on });
6931
7380
  });
7381
+ }).on(GS.setAutoCompact, ({ evt, value }) => {
7382
+ if (!isAdmin(cfg, evt.operator?.openId ?? "")) return;
7383
+ const on = value.v === "on";
7384
+ patch(evt, async () => {
7385
+ const project = await getProjectByChatId(evt.chatId);
7386
+ if (project) {
7387
+ await updateProject(project.name, { autoCompact: on });
7388
+ await evictLiveSessionsForChat(project.chatId);
7389
+ log.info("console", "group-autocompact", { project: project.name, on });
7390
+ return buildGroupSettingsCard({ ...project, autoCompact: on });
7391
+ }
7392
+ return buildGroupSettingsCard({ name: "\u672C\u7FA4", kind: "multi", autoCompact: on });
7393
+ });
6932
7394
  }).on(DM.admins, ({ evt }) => {
6933
7395
  if (!dmAdmin(evt.operator?.openId)) return;
6934
7396
  patch(
@@ -7020,6 +7482,15 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7020
7482
  const p = await getProjectByName(name);
7021
7483
  return p ? buildProjectSettingsCard(p) : buildDmMenuCard();
7022
7484
  });
7485
+ }).on(DM.projectTopics, ({ evt, value }) => {
7486
+ if (!dmAdmin(evt.operator?.openId)) return;
7487
+ const name = typeof value.n === "string" ? value.n : "";
7488
+ patch(evt, async () => {
7489
+ const p = await getProjectByName(name);
7490
+ if (!p) return buildDmMenuCard();
7491
+ const sessions2 = (await listSessions()).filter((s) => s.chatId === p.chatId);
7492
+ return buildProjectTopicsCard(p, sessions2);
7493
+ });
7023
7494
  }).on(DM.setNoMentionDm, ({ evt, value }) => {
7024
7495
  if (!dmAdmin(evt.operator?.openId)) return;
7025
7496
  const name = typeof value.n === "string" ? value.n : "";
@@ -7030,6 +7501,18 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7030
7501
  await updateProject(name, { noMention: on });
7031
7502
  return buildProjectSettingsCard({ ...p, noMention: on });
7032
7503
  });
7504
+ }).on(DM.setAutoCompactDm, ({ evt, value }) => {
7505
+ if (!dmAdmin(evt.operator?.openId)) return;
7506
+ const name = typeof value.n === "string" ? value.n : "";
7507
+ const on = value.v === "on";
7508
+ patch(evt, async () => {
7509
+ const p = await getProjectByName(name);
7510
+ if (!p) return buildDmMenuCard();
7511
+ await updateProject(name, { autoCompact: on });
7512
+ await evictLiveSessionsForChat(p.chatId);
7513
+ log.info("console", "project-autocompact", { project: name, on });
7514
+ return buildProjectSettingsCard({ ...p, autoCompact: on });
7515
+ });
7033
7516
  }).on(DM.permission, ({ evt, value }) => {
7034
7517
  if (!dmAdmin(evt.operator?.openId)) return;
7035
7518
  const name = typeof value.n === "string" ? value.n : "";
@@ -7221,6 +7704,14 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7221
7704
  }
7222
7705
  lastEvAt = tEv;
7223
7706
  evCount++;
7707
+ if (et === "context_usage" && topicThreadId) {
7708
+ const cu = ev;
7709
+ lastUsage.set(topicThreadId, { used: cu.usedTokens, window: cu.contextWindow });
7710
+ } else if (et === "context_compacted") {
7711
+ void sendManagedCard(channel, opts.chatId, buildAutoCompactCard(), cardMsgId, !opts.flat).catch(
7712
+ (err) => log.fail("card", err, { phase: "auto-compact-notice" })
7713
+ );
7714
+ }
7224
7715
  render.apply(ev);
7225
7716
  rc.rs = render.snapshot();
7226
7717
  stream2.streamCoalesced(channel, buildRunCard(rc), ANSWER_EID);
@@ -7293,6 +7784,184 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7293
7784
  release();
7294
7785
  }
7295
7786
  }
7787
+ async function launchGoalRun(opts) {
7788
+ const objective = opts.firstText;
7789
+ const release = await sema.acquire();
7790
+ let activeKey = opts.knownThreadId ?? `pending:${opts.replyTo}`;
7791
+ let topicThreadId = opts.knownThreadId;
7792
+ const state = active.get(activeKey) ?? { queue: [], requesterOpenId: opts.requesterOpenId };
7793
+ state.thread = opts.thread;
7794
+ if (opts.requesterOpenId) state.requesterOpenId = opts.requesterOpenId;
7795
+ active.set(activeKey, state);
7796
+ if (opts.knownThreadId) sessions.set(opts.knownThreadId, opts.thread);
7797
+ const persist = async (threadId) => {
7798
+ await upsertSession({
7799
+ threadId,
7800
+ chatId: opts.chatId,
7801
+ cwd: opts.cwd ?? fallbackCwd,
7802
+ codexThreadId: opts.thread.codexThreadId,
7803
+ model: opts.model,
7804
+ effort: opts.effort,
7805
+ summary: opts.summary ?? objective.slice(0, 80),
7806
+ createdAt: Date.now(),
7807
+ updatedAt: Date.now()
7808
+ }).catch(() => void 0);
7809
+ };
7810
+ let cur = null;
7811
+ let replyTo = opts.replyTo;
7812
+ let replyInThread = opts.flat ? false : opts.replyInThread ?? Boolean(opts.knownThreadId);
7813
+ const adoptThreadId = async (messageId, card2) => {
7814
+ if (activeKey.startsWith("pending:")) {
7815
+ const tid = await getThreadId(channel, messageId);
7816
+ if (tid) {
7817
+ const key = opts.roleSuffix ? `${tid}#${opts.roleSuffix}` : tid;
7818
+ active.delete(activeKey);
7819
+ active.set(key, state);
7820
+ sessions.set(key, opts.thread);
7821
+ activeKey = key;
7822
+ topicThreadId = key;
7823
+ card2.threadId = key;
7824
+ await persist(key);
7825
+ }
7826
+ } else {
7827
+ topicThreadId = activeKey;
7828
+ card2.threadId = activeKey;
7829
+ }
7830
+ };
7831
+ const promoteCard = (msgId, card2) => {
7832
+ if (!topicThreadId) return;
7833
+ const prev = lastRunCard.get(topicThreadId);
7834
+ if (prev && prev !== msgId) {
7835
+ const prevState = runCards.get(prev);
7836
+ const prevStream = runStreams.get(prev);
7837
+ if (prevState && prevStream) void prevStream.updateCard(channel, buildRunCardPlain(prevState));
7838
+ runCards.delete(prev);
7839
+ runStreams.delete(prev);
7840
+ }
7841
+ lastRunCard.set(topicThreadId, msgId);
7842
+ runCards.set(msgId, card2);
7843
+ };
7844
+ const finalizeCard = async (ctx) => {
7845
+ if (!ctx || !ctx.stream || !ctx.cardMsgId) return;
7846
+ await ctx.stream.drain();
7847
+ ctx.render.finalize();
7848
+ ctx.rc.rs = ctx.render.snapshot();
7849
+ await ctx.stream.updateCard(channel, buildRunCard(ctx.rc));
7850
+ runsByCard.delete(ctx.cardMsgId);
7851
+ promoteCard(ctx.cardMsgId, ctx.rc);
7852
+ };
7853
+ const startTurn = () => {
7854
+ const render = new RunRender();
7855
+ render.showTools = getShowToolCalls(cfg);
7856
+ const rc = { rs: render.snapshot(), requesterOpenId: opts.requesterOpenId, showTools: render.showTools, hideStop: true };
7857
+ return { render, rc, stream: null, cardMsgId: null };
7858
+ };
7859
+ const ensureCard = async (ctx) => {
7860
+ if (ctx.stream) return;
7861
+ const stream2 = new RunCardStream();
7862
+ const cardMsgId = await stream2.create(channel, opts.chatId, buildRunCard(ctx.rc), { replyTo, replyInThread });
7863
+ ctx.rc.cardKey = cardMsgId;
7864
+ ctx.stream = stream2;
7865
+ ctx.cardMsgId = cardMsgId;
7866
+ runsByCard.set(cardMsgId, state);
7867
+ runStreams.set(cardMsgId, stream2);
7868
+ await adoptThreadId(cardMsgId, ctx.rc);
7869
+ replyTo = cardMsgId;
7870
+ replyInThread = !opts.flat;
7871
+ };
7872
+ let lastStatus = "active";
7873
+ let goalTokens = 0;
7874
+ let goalSeconds = 0;
7875
+ let goalErrorMsg;
7876
+ let capped = false;
7877
+ let resolveCap;
7878
+ const capSignal = new Promise((res) => {
7879
+ resolveCap = res;
7880
+ });
7881
+ const capTimer = setTimeout(() => {
7882
+ capped = true;
7883
+ resolveCap();
7884
+ }, GOAL_MAX_MS);
7885
+ try {
7886
+ const run = opts.thread.runGoal(objective);
7887
+ state.run = run;
7888
+ const guarded = withIdleTimeout(run.events, 0, () => void 0, capSignal);
7889
+ for await (const ev of guarded) {
7890
+ if (ev.type === "goal_update") {
7891
+ lastStatus = ev.status;
7892
+ goalTokens = ev.tokensUsed;
7893
+ goalSeconds = ev.timeUsedSeconds;
7894
+ continue;
7895
+ }
7896
+ if (ev.type === "context_usage") {
7897
+ if (topicThreadId) lastUsage.set(topicThreadId, { used: ev.usedTokens, window: ev.contextWindow });
7898
+ if (cur) {
7899
+ cur.render.apply(ev);
7900
+ cur.rc.rs = cur.render.snapshot();
7901
+ }
7902
+ continue;
7903
+ }
7904
+ if (ev.type === "context_compacted") {
7905
+ void sendManagedCard(channel, opts.chatId, buildAutoCompactCard(), cur?.cardMsgId ?? void 0, !opts.flat).catch(
7906
+ (err) => log.fail("card", err, { phase: "auto-compact-notice" })
7907
+ );
7908
+ continue;
7909
+ }
7910
+ if (ev.type === "turn_started") {
7911
+ await finalizeCard(cur);
7912
+ cur = startTurn();
7913
+ continue;
7914
+ }
7915
+ if (ev.type === "done") {
7916
+ if (cur) {
7917
+ cur.render.apply(ev);
7918
+ await finalizeCard(cur);
7919
+ cur = null;
7920
+ }
7921
+ continue;
7922
+ }
7923
+ if (ev.type === "error") {
7924
+ goalErrorMsg = ev.message;
7925
+ if (!cur) continue;
7926
+ }
7927
+ if (!cur) cur = startTurn();
7928
+ cur.render.apply(ev);
7929
+ if (ev.type === "thinking" || ev.type === "thinking_delta") {
7930
+ if (cur.stream) {
7931
+ cur.rc.rs = cur.render.snapshot();
7932
+ cur.stream.streamCoalesced(channel, buildRunCard(cur.rc), ANSWER_EID);
7933
+ }
7934
+ continue;
7935
+ }
7936
+ await ensureCard(cur);
7937
+ cur.rc.rs = cur.render.snapshot();
7938
+ cur.stream.streamCoalesced(channel, buildRunCard(cur.rc), ANSWER_EID);
7939
+ }
7940
+ await finalizeCard(cur);
7941
+ cur = null;
7942
+ await opts.thread.clearGoal().catch(() => void 0);
7943
+ const status = capped ? "timeout" : goalErrorMsg && !isGoalTerminal(lastStatus) ? "error" : lastStatus;
7944
+ await sendManagedCard(
7945
+ channel,
7946
+ opts.chatId,
7947
+ buildGoalDoneCard({ objective, status, tokensUsed: goalTokens, timeUsedSeconds: goalSeconds, errorMessage: goalErrorMsg }),
7948
+ replyTo,
7949
+ !opts.flat
7950
+ ).catch((err) => log.fail("card", err, { phase: "goal-done" }));
7951
+ if (topicThreadId) await patchSession(topicThreadId, { updatedAt: Date.now() }).catch(() => void 0);
7952
+ log.info("card", "goal-final", { status, tokens: goalTokens, seconds: goalSeconds });
7953
+ } catch (err) {
7954
+ log.fail("intake", err);
7955
+ await channel.send(opts.chatId, { markdown: `\u274C ${err instanceof Error ? err.message : String(err)}` }, { replyTo: opts.replyTo, replyInThread: !opts.flat }).catch(() => void 0);
7956
+ } finally {
7957
+ clearTimeout(capTimer);
7958
+ active.delete(activeKey);
7959
+ if (cur?.cardMsgId) runsByCard.delete(cur.cardMsgId);
7960
+ void opts.thread.close().catch(() => void 0);
7961
+ if (topicThreadId) sessions.delete(topicThreadId);
7962
+ release();
7963
+ }
7964
+ }
7296
7965
  const onComment = async (evt) => {
7297
7966
  await withTrace({ chatId: "comment" }, async () => {
7298
7967
  log.info("comment", "enter", {