@modelzen/feishu-codex-bridge 0.3.9 → 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 +373 -44
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -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
  }
@@ -1390,6 +1402,18 @@ function mapNotification(n) {
1390
1402
  return { type: "context_compacted" };
1391
1403
  case "turn/completed":
1392
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.
1393
1417
  case "error":
1394
1418
  return { type: "error", message: n.params.error.message, willRetry: n.params.willRetry };
1395
1419
  default:
@@ -1553,6 +1577,64 @@ var CodexThread = class {
1553
1577
  }
1554
1578
  return { events: gen(), turnId: () => self.currentTurnId };
1555
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
+ }
1556
1638
  async steer(input2, expectedTurnId) {
1557
1639
  await this.client.request("turn/steer", {
1558
1640
  threadId: this.codexThreadId,
@@ -2819,7 +2901,7 @@ function renderRunning(state, rc) {
2819
2901
  const answer = textParts.join("\n\n");
2820
2902
  if (answer) elements.push(mdStream(answer, ANSWER_EID));
2821
2903
  if (state.footer) elements.push(footerStatus(state.footer));
2822
- 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")]));
2823
2905
  const gauge = gaugeEl(state);
2824
2906
  if (gauge) elements.push(gauge);
2825
2907
  return elements;
@@ -2969,6 +3051,46 @@ function truncate4(s, n) {
2969
3051
  return s.length > n ? `${s.slice(0, n)}\u2026` : s;
2970
3052
  }
2971
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
+
2972
3094
  // src/card/run-card-stream.ts
2973
3095
  var STREAM_THROTTLE_MS = 150;
2974
3096
  var RunCardStream = class {
@@ -6318,6 +6440,11 @@ function selectValue(formValue, name) {
6318
6440
  function asTier(v) {
6319
6441
  return v === "qa" || v === "write" || v === "full" ? v : void 0;
6320
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
+ }
6321
6448
  function createOrchestrator(channel, cfg, fallbackCwd) {
6322
6449
  const backend = createBackend();
6323
6450
  const sessions = /* @__PURE__ */ new Map();
@@ -6413,6 +6540,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6413
6540
  }
6414
6541
  const text = msg.content.trim();
6415
6542
  const cmd = parseCommand(text);
6543
+ const goalObjective = parseGoalTrigger(text);
6416
6544
  if ((project?.kind ?? "multi") === "single") {
6417
6545
  if (cmd === "help") {
6418
6546
  await postHelpCard(msg, "single", false, project);
@@ -6435,6 +6563,11 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6435
6563
  await postContextCard(msg, ts.sessionKey, false);
6436
6564
  return;
6437
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
+ }
6438
6571
  handleTurn(msg, text, ts.sessionKey, true, project, ts);
6439
6572
  return;
6440
6573
  }
@@ -6456,6 +6589,11 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6456
6589
  await postContextCard(msg, ts.sessionKey, true);
6457
6590
  return;
6458
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
+ }
6459
6597
  handleTurn(msg, text, ts.sessionKey, false, project, ts);
6460
6598
  return;
6461
6599
  }
@@ -6475,6 +6613,11 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6475
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);
6476
6614
  return;
6477
6615
  }
6616
+ if (goalObjective) {
6617
+ void addReaction(msg.messageId, "OKR");
6618
+ startTopicDirectly(msg, goalObjective, project, true);
6619
+ return;
6620
+ }
6478
6621
  startTopicDirectly(msg, text, project);
6479
6622
  };
6480
6623
  function parseCommand(text) {
@@ -6486,7 +6629,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6486
6629
  if (!(project.noMention ?? defaultNoMention(project))) return false;
6487
6630
  if (msg.mentionAll || msg.mentions.some((m) => !m.isBot)) return false;
6488
6631
  if ((project.kind ?? "multi") === "single") return true;
6489
- 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;
6490
6634
  }
6491
6635
  async function denyAdminCommand(msg, cmd) {
6492
6636
  await channel.send(msg.chatId, { markdown: `\u26A0\uFE0F \`/${cmd}\` \u4EC5 bot \u7BA1\u7406\u5458\u53EF\u7528\u3002` }, { replyTo: msg.messageId }).catch(() => void 0);
@@ -6558,9 +6702,13 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6558
6702
  }
6559
6703
  startReservedRun(msg, text, sessionKey, flat, project, perm);
6560
6704
  }
6561
- function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages, preIngested, summaryText2) {
6705
+ function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages, preIngested, summaryText2, goal) {
6562
6706
  const existing = active.get(sessionKey);
6563
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
+ }
6564
6712
  existing.queue.push({ text, images: preloadedImages });
6565
6713
  log.info("intake", "queued", { depth: existing.queue.length });
6566
6714
  return;
@@ -6568,7 +6716,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6568
6716
  const reserved = { queue: [], requesterOpenId: msg.senderId };
6569
6717
  active.set(sessionKey, reserved);
6570
6718
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
6571
- const reaction = runReaction(msg.messageId, !sema.hasFree());
6719
+ const reaction = goal ? void 0 : runReaction(msg.messageId, !sema.hasFree());
6572
6720
  try {
6573
6721
  const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
6574
6722
  let firstText = preIngested ? text : await ingestContext(msg, text);
@@ -6599,7 +6747,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6599
6747
  updatedAt: Date.now()
6600
6748
  });
6601
6749
  }
6602
- if (msg.threadId && (codexEmpty || prior?.lastSeenAt !== void 0)) {
6750
+ if (!goal && msg.threadId && (codexEmpty || prior?.lastSeenAt !== void 0)) {
6603
6751
  const history = await fetchThreadContext(channel, msg.threadId, {
6604
6752
  sinceTime: codexEmpty ? 0 : prior?.lastSeenAt ?? 0,
6605
6753
  excludeMessageId: msg.messageId
@@ -6608,23 +6756,22 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6608
6756
  }
6609
6757
  if (!neverSeen) void patchSession(sessionKey, { lastSeenAt: msg.createTime }).catch(() => void 0);
6610
6758
  reserved.thread = thread;
6611
- await launchRun(
6612
- {
6613
- chatId: msg.chatId,
6614
- replyTo: msg.messageId,
6615
- replyInThread: !flat,
6616
- flat,
6617
- thread,
6618
- firstText,
6619
- images,
6620
- knownThreadId: sessionKey,
6621
- requesterOpenId: msg.senderId
6622
- },
6623
- reaction
6624
- );
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);
6625
6772
  } catch (err) {
6626
6773
  active.delete(sessionKey);
6627
- reaction.done();
6774
+ reaction?.done();
6628
6775
  log.fail("intake", err);
6629
6776
  await channel.send(msg.chatId, { markdown: `\u274C ${err instanceof Error ? err.message : String(err)}` }, { replyTo: msg.messageId, replyInThread: !flat }).catch(() => void 0);
6630
6777
  }
@@ -6676,9 +6823,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6676
6823
  }
6677
6824
  if (closed) log.info("console", "tier-evict", { chatId, closed });
6678
6825
  }
6679
- function startTopicDirectly(msg, text, project) {
6826
+ function startTopicDirectly(msg, text, project, goal) {
6680
6827
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
6681
- const reaction = runReaction(msg.messageId, !sema.hasFree());
6828
+ const reaction = goal ? void 0 : runReaction(msg.messageId, !sema.hasFree());
6682
6829
  const cwd = project?.cwd ?? fallbackCwd;
6683
6830
  const perm = turnPerm(project, msg.senderId);
6684
6831
  if (project) void refreshBranch(channel, project).catch(() => void 0);
@@ -6687,33 +6834,36 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6687
6834
  try {
6688
6835
  thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network, autoCompact: perm.autoCompact });
6689
6836
  } catch (err) {
6690
- reaction.done();
6837
+ reaction?.done();
6691
6838
  log.fail("card", err, { phase: "start-topic" });
6692
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);
6693
6840
  return;
6694
6841
  }
6695
6842
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
6696
6843
  const firstText = await ingestContext(msg, text) || "\u4F60\u597D\uFF0C\u6211\u4EEC\u5F00\u59CB\u5427\u3002";
6697
- log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0 });
6698
- await launchRun(
6699
- {
6700
- chatId: msg.chatId,
6701
- replyTo: msg.messageId,
6702
- replyInThread: true,
6703
- thread,
6704
- firstText,
6705
- images,
6706
- model,
6707
- effort,
6708
- cwd,
6709
- summary: stripFileTokens(text).slice(0, 80) || "(\u7A7A)",
6710
- requesterOpenId: msg.senderId,
6711
- roleSuffix: perm.roleSuffix
6712
- },
6713
- reaction,
6714
- () => reaction.done()
6715
- // topic created → ✅ DONE (don't wait for the reply)
6716
- );
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
+ );
6717
6867
  }).catch((err) => log.fail("intake", err));
6718
6868
  }
6719
6869
  async function postResumeCard(msg) {
@@ -6840,6 +6990,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6840
6990
  }
6841
6991
  const dispatcher = new CardDispatcher(channel, cfg);
6842
6992
  const PENDING_TTL_MS = 30 * 6e4;
6993
+ const GOAL_MAX_MS = 30 * 6e4;
6843
6994
  const CARD_SETTLE_MS = 500;
6844
6995
  const settleUpdate = (msgId, c, fallbackChatId) => {
6845
6996
  const armedAt = Date.now();
@@ -7633,6 +7784,184 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7633
7784
  release();
7634
7785
  }
7635
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
+ }
7636
7965
  const onComment = async (evt) => {
7637
7966
  await withTrace({ chatId: "comment" }, async () => {
7638
7967
  log.info("comment", "enter", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelzen/feishu-codex-bridge",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {