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