@modelzen/feishu-codex-bridge 0.3.9 → 0.3.11

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 +426 -45
  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,
@@ -2789,7 +2871,9 @@ function gaugeEl(state) {
2789
2871
  return state.usage ? runCardGauge(state.usage.used, state.usage.window) : null;
2790
2872
  }
2791
2873
  var RC = {
2792
- stop: "run.stop"
2874
+ stop: "run.stop",
2875
+ /** goal-only: clear the goal but let the in-flight turn finish (no auto-continue). */
2876
+ endGoal: "goal.end"
2793
2877
  };
2794
2878
  var ANSWER_EID = "answer";
2795
2879
  var REASONING_MAX = 1500;
@@ -2819,7 +2903,21 @@ function renderRunning(state, rc) {
2819
2903
  const answer = textParts.join("\n\n");
2820
2904
  if (answer) elements.push(mdStream(answer, ANSWER_EID));
2821
2905
  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")]));
2906
+ if (rc.cardKey && rc.goalControls) {
2907
+ if (rc.goalEnding) {
2908
+ elements.push(noteMd("_\u{1F3AF} \u76EE\u6807\u5DF2\u89E3\u9664\uFF0C\u672C\u8F6E\u8F93\u51FA\u5B8C\u6210\u540E\u505C\u6B62_"));
2909
+ elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
2910
+ } else {
2911
+ elements.push(
2912
+ actions([
2913
+ button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger"),
2914
+ button("\u{1F3AF} \u7ED3\u675F\u76EE\u6807", { a: RC.endGoal, m: rc.cardKey }, "default")
2915
+ ])
2916
+ );
2917
+ }
2918
+ } else if (rc.cardKey && !rc.hideStop) {
2919
+ elements.push(actions([button("\u23F9 \u7EC8\u6B62", { a: RC.stop, m: rc.cardKey }, "danger")]));
2920
+ }
2823
2921
  const gauge = gaugeEl(state);
2824
2922
  if (gauge) elements.push(gauge);
2825
2923
  return elements;
@@ -2969,6 +3067,46 @@ function truncate4(s, n) {
2969
3067
  return s.length > n ? `${s.slice(0, n)}\u2026` : s;
2970
3068
  }
2971
3069
 
3070
+ // src/card/goal-card.ts
3071
+ function fmtTokens(n) {
3072
+ return Math.max(0, Math.round(n)).toLocaleString("en-US");
3073
+ }
3074
+ function fmtDuration(seconds) {
3075
+ const s = Math.max(0, Math.round(seconds));
3076
+ if (s < 60) return `\u7EA6 ${s} \u79D2`;
3077
+ const m = Math.floor(s / 60);
3078
+ const rem = s % 60;
3079
+ if (m < 60) return rem ? `\u7EA6 ${m} \u5206 ${rem} \u79D2` : `\u7EA6 ${m} \u5206`;
3080
+ const h = Math.floor(m / 60);
3081
+ const mm = m % 60;
3082
+ return mm ? `\u7EA6 ${h} \u65F6 ${mm} \u5206` : `\u7EA6 ${h} \u65F6`;
3083
+ }
3084
+ var ABNORMAL_REASON = {
3085
+ budgetLimited: "Token \u9884\u7B97\u7528\u5C3D",
3086
+ usageLimited: "\u8D26\u53F7\u7528\u91CF\u989D\u5EA6\u7528\u5C3D",
3087
+ blocked: "\u88AB\u963B\u585E\uFF0C\u9700\u4EBA\u5DE5\u4ECB\u5165",
3088
+ paused: "\u5DF2\u6682\u505C",
3089
+ timeout: "\u8FD0\u884C\u8D85\u8FC7\u65F6\u957F\u4E0A\u9650\u88AB\u4E2D\u6B62",
3090
+ error: "\u8FD0\u884C\u51FA\u9519"
3091
+ };
3092
+ function buildGoalDoneCard(d) {
3093
+ const ok = isGoalSuccess(d.status);
3094
+ const elements = [
3095
+ md(d.objective.trim() || "\uFF08\u65E0\u76EE\u6807\u63CF\u8FF0\uFF09"),
3096
+ hr(),
3097
+ note(`\u7528\u91CF\u3000${fmtTokens(d.tokensUsed)} tokens`),
3098
+ note(`\u8017\u65F6\u3000${fmtDuration(d.timeUsedSeconds)}`)
3099
+ ];
3100
+ if (!ok) {
3101
+ const reason = d.errorMessage?.trim() || ABNORMAL_REASON[d.status] || `\u72B6\u6001\uFF1A${d.status}`;
3102
+ elements.push(note(`\u539F\u56E0\u3000${reason}`));
3103
+ }
3104
+ return card(elements, {
3105
+ header: ok ? { title: "\u{1F3AF} \u76EE\u6807\u5DF2\u5B8C\u6210", template: "green" } : { title: "\u{1F3AF} \u76EE\u6807\u5DF2\u4E2D\u6B62", template: "orange" },
3106
+ summary: ok ? "\u76EE\u6807\u5DF2\u5B8C\u6210" : "\u76EE\u6807\u5DF2\u4E2D\u6B62"
3107
+ });
3108
+ }
3109
+
2972
3110
  // src/card/run-card-stream.ts
2973
3111
  var STREAM_THROTTLE_MS = 150;
2974
3112
  var RunCardStream = class {
@@ -6318,6 +6456,11 @@ function selectValue(formValue, name) {
6318
6456
  function asTier(v) {
6319
6457
  return v === "qa" || v === "write" || v === "full" ? v : void 0;
6320
6458
  }
6459
+ function parseGoalTrigger(text) {
6460
+ if (!/(^|\s)\/goal(?=\s|$)/i.test(text)) return null;
6461
+ const objective = text.replace(/(^|\s)\/goal(?=\s|$)/gi, " ").replace(/\s+/g, " ").trim();
6462
+ return objective.length > 0 ? objective : null;
6463
+ }
6321
6464
  function createOrchestrator(channel, cfg, fallbackCwd) {
6322
6465
  const backend = createBackend();
6323
6466
  const sessions = /* @__PURE__ */ new Map();
@@ -6413,6 +6556,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6413
6556
  }
6414
6557
  const text = msg.content.trim();
6415
6558
  const cmd = parseCommand(text);
6559
+ const goalObjective = parseGoalTrigger(text);
6416
6560
  if ((project?.kind ?? "multi") === "single") {
6417
6561
  if (cmd === "help") {
6418
6562
  await postHelpCard(msg, "single", false, project);
@@ -6435,6 +6579,11 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6435
6579
  await postContextCard(msg, ts.sessionKey, false);
6436
6580
  return;
6437
6581
  }
6582
+ if (goalObjective) {
6583
+ void addReaction(msg.messageId, "OKR");
6584
+ startReservedRun(msg, goalObjective, ts.sessionKey, true, project, ts, void 0, void 0, void 0, true);
6585
+ return;
6586
+ }
6438
6587
  handleTurn(msg, text, ts.sessionKey, true, project, ts);
6439
6588
  return;
6440
6589
  }
@@ -6456,6 +6605,11 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6456
6605
  await postContextCard(msg, ts.sessionKey, true);
6457
6606
  return;
6458
6607
  }
6608
+ if (goalObjective) {
6609
+ void addReaction(msg.messageId, "OKR");
6610
+ startReservedRun(msg, goalObjective, ts.sessionKey, false, project, ts, void 0, void 0, void 0, true);
6611
+ return;
6612
+ }
6459
6613
  handleTurn(msg, text, ts.sessionKey, false, project, ts);
6460
6614
  return;
6461
6615
  }
@@ -6475,6 +6629,11 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6475
6629
  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
6630
  return;
6477
6631
  }
6632
+ if (goalObjective) {
6633
+ void addReaction(msg.messageId, "OKR");
6634
+ startTopicDirectly(msg, goalObjective, project, true);
6635
+ return;
6636
+ }
6478
6637
  startTopicDirectly(msg, text, project);
6479
6638
  };
6480
6639
  function parseCommand(text) {
@@ -6486,7 +6645,8 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6486
6645
  if (!(project.noMention ?? defaultNoMention(project))) return false;
6487
6646
  if (msg.mentionAll || msg.mentions.some((m) => !m.isBot)) return false;
6488
6647
  if ((project.kind ?? "multi") === "single") return true;
6489
- return Boolean(msg.threadId) || parseCommand(msg.content.trim()) !== null;
6648
+ const content = msg.content.trim();
6649
+ return Boolean(msg.threadId) || parseCommand(content) !== null || parseGoalTrigger(content) !== null;
6490
6650
  }
6491
6651
  async function denyAdminCommand(msg, cmd) {
6492
6652
  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 +6718,13 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6558
6718
  }
6559
6719
  startReservedRun(msg, text, sessionKey, flat, project, perm);
6560
6720
  }
6561
- function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages, preIngested, summaryText2) {
6721
+ function startReservedRun(msg, text, sessionKey, flat, project, perm, preloadedImages, preIngested, summaryText2, goal) {
6562
6722
  const existing = active.get(sessionKey);
6563
6723
  if (existing) {
6724
+ if (goal) {
6725
+ 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);
6726
+ return;
6727
+ }
6564
6728
  existing.queue.push({ text, images: preloadedImages });
6565
6729
  log.info("intake", "queued", { depth: existing.queue.length });
6566
6730
  return;
@@ -6568,7 +6732,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6568
6732
  const reserved = { queue: [], requesterOpenId: msg.senderId };
6569
6733
  active.set(sessionKey, reserved);
6570
6734
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
6571
- const reaction = runReaction(msg.messageId, !sema.hasFree());
6735
+ const reaction = goal ? void 0 : runReaction(msg.messageId, !sema.hasFree());
6572
6736
  try {
6573
6737
  const images = preloadedImages ?? (messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0);
6574
6738
  let firstText = preIngested ? text : await ingestContext(msg, text);
@@ -6599,7 +6763,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6599
6763
  updatedAt: Date.now()
6600
6764
  });
6601
6765
  }
6602
- if (msg.threadId && (codexEmpty || prior?.lastSeenAt !== void 0)) {
6766
+ if (!goal && msg.threadId && (codexEmpty || prior?.lastSeenAt !== void 0)) {
6603
6767
  const history = await fetchThreadContext(channel, msg.threadId, {
6604
6768
  sinceTime: codexEmpty ? 0 : prior?.lastSeenAt ?? 0,
6605
6769
  excludeMessageId: msg.messageId
@@ -6608,23 +6772,22 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6608
6772
  }
6609
6773
  if (!neverSeen) void patchSession(sessionKey, { lastSeenAt: msg.createTime }).catch(() => void 0);
6610
6774
  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
- );
6775
+ const launchOpts = {
6776
+ chatId: msg.chatId,
6777
+ replyTo: msg.messageId,
6778
+ replyInThread: !flat,
6779
+ flat,
6780
+ thread,
6781
+ firstText,
6782
+ images,
6783
+ knownThreadId: sessionKey,
6784
+ requesterOpenId: msg.senderId
6785
+ };
6786
+ if (goal) await launchGoalRun(launchOpts);
6787
+ else await launchRun(launchOpts, reaction);
6625
6788
  } catch (err) {
6626
6789
  active.delete(sessionKey);
6627
- reaction.done();
6790
+ reaction?.done();
6628
6791
  log.fail("intake", err);
6629
6792
  await channel.send(msg.chatId, { markdown: `\u274C ${err instanceof Error ? err.message : String(err)}` }, { replyTo: msg.messageId, replyInThread: !flat }).catch(() => void 0);
6630
6793
  }
@@ -6676,9 +6839,9 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6676
6839
  }
6677
6840
  if (closed) log.info("console", "tier-evict", { chatId, closed });
6678
6841
  }
6679
- function startTopicDirectly(msg, text, project) {
6842
+ function startTopicDirectly(msg, text, project, goal) {
6680
6843
  void withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
6681
- const reaction = runReaction(msg.messageId, !sema.hasFree());
6844
+ const reaction = goal ? void 0 : runReaction(msg.messageId, !sema.hasFree());
6682
6845
  const cwd = project?.cwd ?? fallbackCwd;
6683
6846
  const perm = turnPerm(project, msg.senderId);
6684
6847
  if (project) void refreshBranch(channel, project).catch(() => void 0);
@@ -6687,33 +6850,36 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6687
6850
  try {
6688
6851
  thread = await backend.startThread({ cwd, model, effort, mode: perm.mode, network: perm.network, autoCompact: perm.autoCompact });
6689
6852
  } catch (err) {
6690
- reaction.done();
6853
+ reaction?.done();
6691
6854
  log.fail("card", err, { phase: "start-topic" });
6692
6855
  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
6856
  return;
6694
6857
  }
6695
6858
  const images = messageHasImages(msg) ? await collectInboundImages(channel, msg) : void 0;
6696
6859
  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
- );
6860
+ log.info("card", "start", { project: project?.name ?? "(unregistered)", model, effort, images: images?.length ?? 0, goal: Boolean(goal) });
6861
+ const launchOpts = {
6862
+ chatId: msg.chatId,
6863
+ replyTo: msg.messageId,
6864
+ replyInThread: true,
6865
+ thread,
6866
+ firstText,
6867
+ images,
6868
+ model,
6869
+ effort,
6870
+ cwd,
6871
+ summary: stripFileTokens(text).slice(0, 80) || "(\u7A7A)",
6872
+ requesterOpenId: msg.senderId,
6873
+ roleSuffix: perm.roleSuffix
6874
+ };
6875
+ if (goal) await launchGoalRun(launchOpts);
6876
+ else
6877
+ await launchRun(
6878
+ launchOpts,
6879
+ reaction,
6880
+ () => reaction?.done()
6881
+ // topic created → ✅ DONE (don't wait for the reply)
6882
+ );
6717
6883
  }).catch((err) => log.fail("intake", err));
6718
6884
  }
6719
6885
  async function postResumeCard(msg) {
@@ -6840,6 +7006,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6840
7006
  }
6841
7007
  const dispatcher = new CardDispatcher(channel, cfg);
6842
7008
  const PENDING_TTL_MS = 30 * 6e4;
7009
+ const GOAL_IDLE_MS = 30 * 6e4;
6843
7010
  const CARD_SETTLE_MS = 500;
6844
7011
  const settleUpdate = (msgId, c, fallbackChatId) => {
6845
7012
  const armedAt = Date.now();
@@ -6921,6 +7088,12 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
6921
7088
  if (!st || !runOwnerOrAdmin(evt, st.requesterOpenId)) return;
6922
7089
  st.interrupt?.();
6923
7090
  log.info("card", "action", { actionId: "run.stop", stopped: Boolean(st.interrupt) });
7091
+ }).on(RC.endGoal, ({ evt, value }) => {
7092
+ const key = typeof value.m === "string" ? value.m : evt.messageId;
7093
+ const st = runsByCard.get(key);
7094
+ if (!st || !runOwnerOrAdmin(evt, st.requesterOpenId)) return;
7095
+ st.endGoal?.();
7096
+ log.info("card", "action", { actionId: "goal.end", ended: Boolean(st.endGoal) });
6924
7097
  });
6925
7098
  const dmAdmin = (openId) => isAdmin(cfg, openId ?? "");
6926
7099
  const patch = (evt, c) => settleUpdate(evt.messageId, c, evt.chatId);
@@ -7633,6 +7806,214 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
7633
7806
  release();
7634
7807
  }
7635
7808
  }
7809
+ async function launchGoalRun(opts) {
7810
+ const objective = opts.firstText;
7811
+ const release = await sema.acquire();
7812
+ let activeKey = opts.knownThreadId ?? `pending:${opts.replyTo}`;
7813
+ let topicThreadId = opts.knownThreadId;
7814
+ const state = active.get(activeKey) ?? { queue: [], requesterOpenId: opts.requesterOpenId };
7815
+ state.thread = opts.thread;
7816
+ if (opts.requesterOpenId) state.requesterOpenId = opts.requesterOpenId;
7817
+ active.set(activeKey, state);
7818
+ if (opts.knownThreadId) sessions.set(opts.knownThreadId, opts.thread);
7819
+ const persist = async (threadId) => {
7820
+ await upsertSession({
7821
+ threadId,
7822
+ chatId: opts.chatId,
7823
+ cwd: opts.cwd ?? fallbackCwd,
7824
+ codexThreadId: opts.thread.codexThreadId,
7825
+ model: opts.model,
7826
+ effort: opts.effort,
7827
+ summary: opts.summary ?? objective.slice(0, 80),
7828
+ createdAt: Date.now(),
7829
+ updatedAt: Date.now()
7830
+ }).catch(() => void 0);
7831
+ };
7832
+ let cur = null;
7833
+ let replyTo = opts.replyTo;
7834
+ let replyInThread = opts.flat ? false : opts.replyInThread ?? Boolean(opts.knownThreadId);
7835
+ const adoptThreadId = async (messageId, card2) => {
7836
+ if (activeKey.startsWith("pending:")) {
7837
+ const tid = await getThreadId(channel, messageId);
7838
+ if (tid) {
7839
+ const key = opts.roleSuffix ? `${tid}#${opts.roleSuffix}` : tid;
7840
+ active.delete(activeKey);
7841
+ active.set(key, state);
7842
+ sessions.set(key, opts.thread);
7843
+ activeKey = key;
7844
+ topicThreadId = key;
7845
+ card2.threadId = key;
7846
+ await persist(key);
7847
+ }
7848
+ } else {
7849
+ topicThreadId = activeKey;
7850
+ card2.threadId = activeKey;
7851
+ }
7852
+ };
7853
+ const promoteCard = (msgId, card2) => {
7854
+ if (!topicThreadId) return;
7855
+ const prev = lastRunCard.get(topicThreadId);
7856
+ if (prev && prev !== msgId) {
7857
+ const prevState = runCards.get(prev);
7858
+ const prevStream = runStreams.get(prev);
7859
+ if (prevState && prevStream) void prevStream.updateCard(channel, buildRunCardPlain(prevState));
7860
+ runCards.delete(prev);
7861
+ runStreams.delete(prev);
7862
+ }
7863
+ lastRunCard.set(topicThreadId, msgId);
7864
+ runCards.set(msgId, card2);
7865
+ };
7866
+ const finalizeCard = async (ctx) => {
7867
+ if (!ctx || !ctx.stream || !ctx.cardMsgId) return;
7868
+ await ctx.stream.drain();
7869
+ ctx.render.finalize();
7870
+ ctx.rc.rs = ctx.render.snapshot();
7871
+ await ctx.stream.updateCard(channel, buildRunCard(ctx.rc));
7872
+ runsByCard.delete(ctx.cardMsgId);
7873
+ promoteCard(ctx.cardMsgId, ctx.rc);
7874
+ };
7875
+ const startTurn = () => {
7876
+ const render = new RunRender();
7877
+ render.showTools = getShowToolCalls(cfg);
7878
+ const rc = { rs: render.snapshot(), requesterOpenId: opts.requesterOpenId, showTools: render.showTools, goalControls: true };
7879
+ return { render, rc, stream: null, cardMsgId: null };
7880
+ };
7881
+ const ensureCard = async (ctx) => {
7882
+ if (ctx.stream) return;
7883
+ const stream2 = new RunCardStream();
7884
+ const cardMsgId = await stream2.create(channel, opts.chatId, buildRunCard(ctx.rc), { replyTo, replyInThread });
7885
+ ctx.rc.cardKey = cardMsgId;
7886
+ ctx.stream = stream2;
7887
+ ctx.cardMsgId = cardMsgId;
7888
+ runsByCard.set(cardMsgId, state);
7889
+ runStreams.set(cardMsgId, stream2);
7890
+ await adoptThreadId(cardMsgId, ctx.rc);
7891
+ replyTo = cardMsgId;
7892
+ replyInThread = !opts.flat;
7893
+ };
7894
+ let lastStatus = "active";
7895
+ let goalTokens = 0;
7896
+ let goalSeconds = 0;
7897
+ let goalErrorMsg;
7898
+ let interrupted = false;
7899
+ let goalEnded = false;
7900
+ let idledOut = false;
7901
+ let resolveStop;
7902
+ let resolveEnd;
7903
+ const stopSignal = new Promise((res) => {
7904
+ resolveStop = res;
7905
+ });
7906
+ const endSignal = new Promise((res) => {
7907
+ resolveEnd = res;
7908
+ });
7909
+ try {
7910
+ const run = opts.thread.runGoal(objective);
7911
+ state.run = run;
7912
+ state.interrupt = () => {
7913
+ if (interrupted) return;
7914
+ interrupted = true;
7915
+ void opts.thread.clearGoal().catch(() => void 0);
7916
+ resolveStop();
7917
+ };
7918
+ state.endGoal = () => {
7919
+ if (goalEnded || interrupted) return;
7920
+ goalEnded = true;
7921
+ void opts.thread.clearGoal().catch(() => void 0);
7922
+ if (cur) {
7923
+ cur.rc.goalEnding = true;
7924
+ if (cur.stream) {
7925
+ cur.rc.rs = cur.render.snapshot();
7926
+ cur.stream.streamCoalesced(channel, buildRunCard(cur.rc), ANSWER_EID);
7927
+ }
7928
+ } else {
7929
+ resolveEnd();
7930
+ }
7931
+ };
7932
+ const stop = Promise.race([stopSignal, endSignal]);
7933
+ const guarded = withIdleTimeout(run.events, GOAL_IDLE_MS, () => {
7934
+ idledOut = true;
7935
+ }, stop);
7936
+ for await (const ev of guarded) {
7937
+ if (ev.type === "goal_update") {
7938
+ lastStatus = ev.status;
7939
+ goalTokens = ev.tokensUsed;
7940
+ goalSeconds = ev.timeUsedSeconds;
7941
+ continue;
7942
+ }
7943
+ if (ev.type === "context_usage") {
7944
+ if (topicThreadId) lastUsage.set(topicThreadId, { used: ev.usedTokens, window: ev.contextWindow });
7945
+ if (cur) {
7946
+ cur.render.apply(ev);
7947
+ cur.rc.rs = cur.render.snapshot();
7948
+ }
7949
+ continue;
7950
+ }
7951
+ if (ev.type === "context_compacted") {
7952
+ void sendManagedCard(channel, opts.chatId, buildAutoCompactCard(), cur?.cardMsgId ?? void 0, !opts.flat).catch(
7953
+ (err) => log.fail("card", err, { phase: "auto-compact-notice" })
7954
+ );
7955
+ continue;
7956
+ }
7957
+ if (ev.type === "turn_started") {
7958
+ await finalizeCard(cur);
7959
+ cur = startTurn();
7960
+ continue;
7961
+ }
7962
+ if (ev.type === "done") {
7963
+ if (cur) {
7964
+ cur.render.apply(ev);
7965
+ await finalizeCard(cur);
7966
+ cur = null;
7967
+ }
7968
+ if (goalEnded) break;
7969
+ continue;
7970
+ }
7971
+ if (ev.type === "error") {
7972
+ goalErrorMsg = ev.message;
7973
+ if (!cur) continue;
7974
+ }
7975
+ if (!cur) cur = startTurn();
7976
+ cur.render.apply(ev);
7977
+ if (ev.type === "thinking" || ev.type === "thinking_delta") {
7978
+ if (cur.stream) {
7979
+ cur.rc.rs = cur.render.snapshot();
7980
+ cur.stream.streamCoalesced(channel, buildRunCard(cur.rc), ANSWER_EID);
7981
+ }
7982
+ continue;
7983
+ }
7984
+ await ensureCard(cur);
7985
+ cur.rc.rs = cur.render.snapshot();
7986
+ cur.stream.streamCoalesced(channel, buildRunCard(cur.rc), ANSWER_EID);
7987
+ }
7988
+ if (interrupted && cur) cur.render.interrupt();
7989
+ await finalizeCard(cur);
7990
+ cur = null;
7991
+ await opts.thread.clearGoal().catch(() => void 0);
7992
+ if (!interrupted && !goalEnded) {
7993
+ const status = idledOut ? "timeout" : goalErrorMsg && !isGoalTerminal(lastStatus) ? "error" : lastStatus;
7994
+ await sendManagedCard(
7995
+ channel,
7996
+ opts.chatId,
7997
+ buildGoalDoneCard({ objective, status, tokensUsed: goalTokens, timeUsedSeconds: goalSeconds, errorMessage: goalErrorMsg }),
7998
+ replyTo,
7999
+ !opts.flat
8000
+ ).catch((err) => log.fail("card", err, { phase: "goal-done" }));
8001
+ log.info("card", "goal-final", { status, tokens: goalTokens, seconds: goalSeconds });
8002
+ } else {
8003
+ log.info("card", "goal-final", { status: interrupted ? "interrupted" : "ended", tokens: goalTokens, seconds: goalSeconds });
8004
+ }
8005
+ if (topicThreadId) await patchSession(topicThreadId, { updatedAt: Date.now() }).catch(() => void 0);
8006
+ } catch (err) {
8007
+ log.fail("intake", err);
8008
+ await channel.send(opts.chatId, { markdown: `\u274C ${err instanceof Error ? err.message : String(err)}` }, { replyTo: opts.replyTo, replyInThread: !opts.flat }).catch(() => void 0);
8009
+ } finally {
8010
+ active.delete(activeKey);
8011
+ if (cur?.cardMsgId) runsByCard.delete(cur.cardMsgId);
8012
+ void opts.thread.close().catch(() => void 0);
8013
+ if (topicThreadId) sessions.delete(topicThreadId);
8014
+ release();
8015
+ }
8016
+ }
7636
8017
  const onComment = async (evt) => {
7637
8018
  await withTrace({ chatId: "comment" }, async () => {
7638
8019
  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.11",
4
4
  "description": "Bridge Feishu/Lark messenger with local Codex via app-server (project=group, thread=session)",
5
5
  "type": "module",
6
6
  "bin": {