@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.
- package/dist/cli.js +373 -44
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
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", {
|