sintbot 1.4.8 → 1.4.9
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/CHANGELOG.en.json +19 -0
- package/CHANGELOG.json +19 -0
- package/docs/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/install.md +2 -2
- package/docs/zh-CN/AI-ASSISTED-INSTALL.md +2 -2
- package/docs/zh-CN/install.md +2 -2
- package/install.sh +1 -1
- package/package.json +1 -1
- package/src/agent/privatecore.js +1 -1
- package/src/agent/privatecore.wasm +0 -0
- package/src/core/c2cardterminalpath.js +63 -7
- package/src/core/c2durableruntime.js +48 -4
- package/src/core/c2localeffectexecutor.js +3 -4
- package/src/core/c2terminaleffectbridge.js +12 -1
- package/src/core/v2engine.js +118 -20
- package/src/platform/feishu.js +1 -1
package/CHANGELOG.en.json
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.4.9",
|
|
4
|
+
"date": "2026-11-10",
|
|
5
|
+
"name": "Failure completion and reply card improvements",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"t": "Fix: a reply card could remain in progress after a failed task",
|
|
9
|
+
"d": "After a confirmed executor error and exit, the existing card shows a failure and the group can handle the next message. Without an open card, one failure notice is sent. Uncertain delivery remains held without automatic retries."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"t": "Improve: reply cards show the actual model and reasoning effort",
|
|
13
|
+
"d": "The final card shows the selected model and effort. If an unsupported effort falls back to the model default, it no longer shows that rejected effort. Long durations use minutes or hours, and absent usage no longer appears as zero tokens."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"t": "Fix: answers stayed at the old card position after a busy-turn follow-up",
|
|
17
|
+
"d": "After Codex confirms a follow-up landed, the reply card moves beneath that message. An open card is copied to the new position before the old one is recalled."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
2
21
|
{
|
|
3
22
|
"version": "1.4.8",
|
|
4
23
|
"date": "2026-11-10",
|
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.4.9",
|
|
4
|
+
"date": "2026-11-10",
|
|
5
|
+
"name": "故障收尾与回复卡片优化",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"t": "修复:执行失败后回复卡仍停在处理中",
|
|
9
|
+
"d": "执行器明确报错并确认退出后,原卡片显示失败,群可继续处理下一条消息;没有卡片时只发一条失败提示。发送结果不明时仍保留现场,不自动重试。"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"t": "优化:回复卡片显示实际模型和推理强度",
|
|
13
|
+
"d": "完成时在卡片底部显示本轮选用的型号与强度;如果强度被拒后回退默认,不再显示被拒的强度。长任务耗时改用分和小时,未报告用量时不显示零 token。"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"t": "修复:忙时补话后答案还留在旧卡片位置",
|
|
17
|
+
"d": "Codex 确认收到补话后,回复卡会跟到那条消息下方;卡片已出现时先将内容追平到新卡,再撤回旧卡。"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
2
21
|
{
|
|
3
22
|
"version": "1.4.8",
|
|
4
23
|
"date": "2026-11-10",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Install SintBot 1.4.
|
|
1
|
+
# Install SintBot 1.4.9 with an AI assistant
|
|
2
2
|
|
|
3
3
|
English · [简体中文](./zh-CN/AI-ASSISTED-INSTALL.md)
|
|
4
4
|
|
|
@@ -15,5 +15,5 @@ Use the terminal on the Mac that will run SintBot. A cloud shell cannot install
|
|
|
15
15
|
## Copyable request
|
|
16
16
|
|
|
17
17
|
```text
|
|
18
|
-
Install SintBot 1.4.
|
|
18
|
+
Install SintBot 1.4.9 on this Mac using the one-line installation guide. Preserve existing services, configuration, state and Codex or Claude Code account data. Do not expose credentials. Verify the actual installed executable and binary core, configure the Feishu groups with their runners, and initialize only clean new state. Run the foreground preflight and an authorized read-only group test, check the result, then verify clean stop/restart. Stop temporary processes when finished, and distinguish package installation from successful group acceptance.
|
|
19
19
|
```
|
package/docs/install.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Install SintBot 1.4.
|
|
1
|
+
# Install SintBot 1.4.9
|
|
2
2
|
|
|
3
3
|
English · [简体中文](./zh-CN/install.md)
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ You need a Mac, Node.js 22 or newer, and projects stored on that Mac. Install an
|
|
|
10
10
|
curl -fsSL https://sintbot.com/install.sh | bash
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The installer checks the environment, installs `sintbot@1.4.
|
|
13
|
+
The installer checks the environment, installs `sintbot@1.4.9` into `~/.local/share/sintbot`, and verifies the entry, built-in core and Feishu module. Continue when you see `Installed and verified SintBot 1.4.9`. No sudo is needed.
|
|
14
14
|
|
|
15
15
|
To preview, append `-s -- --dry-run`. To choose another location, set `SINTBOT_INSTALL_DIR` and adjust the executable paths below.
|
|
16
16
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 用 AI 助手安装 SintBot 1.4.
|
|
1
|
+
# 用 AI 助手安装 SintBot 1.4.9
|
|
2
2
|
|
|
3
3
|
[English](../AI-ASSISTED-INSTALL.md) · 简体中文
|
|
4
4
|
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
## 可复制的请求
|
|
16
16
|
|
|
17
17
|
```text
|
|
18
|
-
按一行安装指南,在这台 Mac 安装 SintBot 1.4.
|
|
18
|
+
按一行安装指南,在这台 Mac 安装 SintBot 1.4.9。保留已有服务、配置、状态和 Codex 或 Claude Code 账号数据,不输出凭据。验证实际入口与二进制核心,配置飞书群及各群的执行器,只对全新状态初始化。运行前台预检和已授权的只读群测试,对照结果,再验证正常停止与重启。完成后停止临时进程,并区分程序包安装与群验收结果。
|
|
19
19
|
```
|
package/docs/zh-CN/install.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 安装 SintBot 1.4.
|
|
1
|
+
# 安装 SintBot 1.4.9
|
|
2
2
|
|
|
3
3
|
[English](../install.md) · 简体中文
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
curl -fsSL https://sintbot.com/install.sh | bash
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
安装器会自动检查环境,把 `sintbot@1.4.
|
|
13
|
+
安装器会自动检查环境,把 `sintbot@1.4.9` 装到 `~/.local/share/sintbot`,并验证程序入口、内置核心和飞书模块。看到 `Installed and verified SintBot 1.4.9` 就可以继续。不需要 sudo。
|
|
14
14
|
|
|
15
15
|
可选预览:在命令末尾加 `-s -- --dry-run`。要自定义安装位置,设置 `SINTBOT_INSTALL_DIR` 环境变量,并同步修改后面的可执行文件路径。
|
|
16
16
|
|
package/install.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# The function is parsed in full before installation starts, including when piped.
|
|
3
3
|
sintbot_install() (
|
|
4
4
|
set -eu
|
|
5
|
-
version=1.4.
|
|
5
|
+
version=1.4.9
|
|
6
6
|
prefix="${SINTBOT_INSTALL_DIR:-$HOME/.local/share/sintbot}"
|
|
7
7
|
fail() { printf 'SintBot: %s\n' "$*" >&2; exit 1; }
|
|
8
8
|
case "${1:-}" in
|
package/package.json
CHANGED
package/src/agent/privatecore.js
CHANGED
|
@@ -3,7 +3,7 @@ const fs = require('node:fs')
|
|
|
3
3
|
const path = require('node:path')
|
|
4
4
|
const { createHash } = require('node:crypto')
|
|
5
5
|
|
|
6
|
-
const EXPECTED_SHA256 = '
|
|
6
|
+
const EXPECTED_SHA256 = '70bb1b33ba1d9f3249659a4721ed047350f866dd2b3601a67376df604a23b3fd'
|
|
7
7
|
let bytes
|
|
8
8
|
try {
|
|
9
9
|
bytes = fs.readFileSync(path.join(__dirname, 'privatecore.wasm'))
|
|
Binary file
|
|
@@ -9,6 +9,7 @@ const { prepareTerminalEffectBridge, terminalReplyText } = require('./c2terminal
|
|
|
9
9
|
const { stripMilestones } = require('./turnprogress')
|
|
10
10
|
|
|
11
11
|
function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
12
|
+
cardAnchor = null, preferPlain = false,
|
|
12
13
|
receiptId, chatId, turnToken, generation, verifyGeneration,
|
|
13
14
|
applyLocalEffect, assertWriterLock, sendPlainReply = null, redactText = value => value,
|
|
14
15
|
now = Date.now } = {}) {
|
|
@@ -20,7 +21,8 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
20
21
|
typeof sendPlainReply !== 'function')) {
|
|
21
22
|
throw new TypeError('C2 card terminal path needs one exact message target')
|
|
22
23
|
}
|
|
23
|
-
if (
|
|
24
|
+
if (typeof preferPlain !== 'boolean' ||
|
|
25
|
+
sendPlainReply !== null && typeof sendPlainReply !== 'function') {
|
|
24
26
|
throw new TypeError('C2 plain reply needs one strict sender')
|
|
25
27
|
}
|
|
26
28
|
if (typeof redactText !== 'function' ||
|
|
@@ -28,6 +30,27 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
28
30
|
throw new TypeError('C2 card text redaction must be synchronous')
|
|
29
31
|
}
|
|
30
32
|
const targetCtx = Object.freeze({ ...replyCtx })
|
|
33
|
+
const landedAnchor = anchor => {
|
|
34
|
+
if (!anchor || !/^[0-9a-f]{32}$/.test(anchor.receiptId || '') ||
|
|
35
|
+
anchor.replyCtx?.chatId !== chatId ||
|
|
36
|
+
typeof anchor.replyCtx.messageId !== 'string' ||
|
|
37
|
+
!anchor.replyCtx.messageId) {
|
|
38
|
+
throw new TypeError('C2 card anchor needs one landed follow-up')
|
|
39
|
+
}
|
|
40
|
+
const snapshot = runtime.snapshot()
|
|
41
|
+
const steer = snapshot.state?.lanes?.steer?.[chatId]
|
|
42
|
+
const followup = snapshot.receipts?.pending?.find(item => item.id === anchor.receiptId)
|
|
43
|
+
if (steer?.parentReceiptId !== receiptId || steer.turnToken !== turnToken ||
|
|
44
|
+
steer.generation !== generation ||
|
|
45
|
+
!steer.entries?.some(item => item.receiptId === anchor.receiptId &&
|
|
46
|
+
item.status === 'landed') || followup?.chatId !== chatId ||
|
|
47
|
+
followup.source !== 'message' ||
|
|
48
|
+
followup.sourceId !== anchor.replyCtx.messageId) {
|
|
49
|
+
throw new Error('C2 card anchor differs from landed steer receipt')
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({ ...anchor.replyCtx })
|
|
52
|
+
}
|
|
53
|
+
let activeAnchorCtx = cardAnchor ? landedAnchor(cardAnchor) : targetCtx
|
|
31
54
|
// Registration is durable before open() can expose any runner callback.
|
|
32
55
|
const terminal = prepareTerminalEffectBridge({ runtime, payloadHandles,
|
|
33
56
|
receiptId, chatId, turnToken, generation, verifyGeneration,
|
|
@@ -36,6 +59,7 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
36
59
|
chatId, turnToken, generation, verifyGeneration })
|
|
37
60
|
let phase = 'prepared'
|
|
38
61
|
let card = null
|
|
62
|
+
let lastCardText = ''
|
|
39
63
|
let suppressedCard = false
|
|
40
64
|
const fail = error => { phase = 'closed'; runtime.close(); throw error }
|
|
41
65
|
const canUsePlain = before => {
|
|
@@ -62,13 +86,13 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
62
86
|
}
|
|
63
87
|
phase = 'opening'
|
|
64
88
|
const before = runtime.snapshot()
|
|
65
|
-
if (typeof platform.createStreamingCard !== 'function') {
|
|
89
|
+
if (preferPlain || typeof platform.createStreamingCard !== 'function') {
|
|
66
90
|
if (!canUsePlain(before)) return fail(new Error('C2 plain reply lacks unopened card proof'))
|
|
67
91
|
phase = 'plain'
|
|
68
92
|
return false
|
|
69
93
|
}
|
|
70
94
|
try {
|
|
71
|
-
card = await platform.createStreamingCard(
|
|
95
|
+
card = await platform.createStreamingCard(activeAnchorCtx,
|
|
72
96
|
{ operationKey: receiptId, operationBoundary })
|
|
73
97
|
if (!card && canUsePlain(before)) {
|
|
74
98
|
phase = 'plain'
|
|
@@ -95,7 +119,37 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
95
119
|
}
|
|
96
120
|
if (suppressedCard) throw new Error('C2 suppressed card cannot stream an update')
|
|
97
121
|
if (typeof text !== 'string') throw new TypeError('C2 card update needs process-local text')
|
|
98
|
-
if (card)
|
|
122
|
+
if (card) {
|
|
123
|
+
lastCardText = redactText(text)
|
|
124
|
+
card.update(lastCardText)
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async relocate(anchor) {
|
|
128
|
+
const nextCtx = landedAnchor(anchor)
|
|
129
|
+
if (phase !== 'open' || !card) return false
|
|
130
|
+
if (activeAnchorCtx.messageId === nextCtx.messageId) return true
|
|
131
|
+
if (!lastCardText) throw new Error('C2 card move has no visible text to replay')
|
|
132
|
+
phase = 'relocating'
|
|
133
|
+
try {
|
|
134
|
+
if (await card.flush() !== true) throw new Error('C2 old card flush unconfirmed')
|
|
135
|
+
// A different operation key gives each card its own durable create,
|
|
136
|
+
// post, frame and delete identities under the same running turn.
|
|
137
|
+
const fresh = await platform.createStreamingCard(nextCtx, {
|
|
138
|
+
operationKey: `${receiptId}:followup:${anchor.receiptId}`,
|
|
139
|
+
operationBoundary,
|
|
140
|
+
})
|
|
141
|
+
if (!fresh || typeof fresh.update !== 'function' ||
|
|
142
|
+
typeof fresh.flush !== 'function' || typeof fresh.recall !== 'function') {
|
|
143
|
+
throw new Error('C2 moved card has no durable writer')
|
|
144
|
+
}
|
|
145
|
+
fresh.update(lastCardText)
|
|
146
|
+
if (await fresh.flush() !== true) throw new Error('C2 moved card text unconfirmed')
|
|
147
|
+
if (await card.recall() !== true) throw new Error('C2 old card delete unconfirmed')
|
|
148
|
+
card = fresh
|
|
149
|
+
activeAnchorCtx = nextCtx
|
|
150
|
+
phase = 'open'
|
|
151
|
+
return true
|
|
152
|
+
} catch (error) { return fail(error) }
|
|
99
153
|
},
|
|
100
154
|
async flushForTerminal() {
|
|
101
155
|
if (phase !== 'open' || !card || await card.flush() !== true) {
|
|
@@ -131,7 +185,8 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
131
185
|
return true
|
|
132
186
|
} catch (error) { return fail(error) }
|
|
133
187
|
},
|
|
134
|
-
async complete({ payload, replyText, paused = false, footer = '',
|
|
188
|
+
async complete({ payload, replyText, paused = false, footer = '',
|
|
189
|
+
footerDetail = '', milestones = [] } = {}) {
|
|
135
190
|
if (phase !== 'open' && phase !== 'plain') {
|
|
136
191
|
throw new Error('C2 card terminal path is not open')
|
|
137
192
|
}
|
|
@@ -144,7 +199,8 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
144
199
|
typeof terminalPayload.quiet !== 'boolean' || terminalPayload.quiet !== receipt.quiet ||
|
|
145
200
|
typeof terminalPayload.resultText !== 'string' ||
|
|
146
201
|
typeof terminalPayload.bufferText !== 'string' ||
|
|
147
|
-
typeof footer !== 'string' ||
|
|
202
|
+
typeof footer !== 'string' || typeof footerDetail !== 'string' ||
|
|
203
|
+
!Array.isArray(milestones) ||
|
|
148
204
|
!milestones.every(item => typeof item === 'string' && item.trim())) {
|
|
149
205
|
throw new TypeError('C2 card terminal payload differs from running turn')
|
|
150
206
|
}
|
|
@@ -190,7 +246,7 @@ function prepareCardTerminalPath({ runtime, payloadHandles, platform, replyCtx,
|
|
|
190
246
|
// card's "process" fold; an answer that was only milestones shows
|
|
191
247
|
// them as its body. The durable reply text stays exact.
|
|
192
248
|
let shownText = visibleText
|
|
193
|
-
const finishOpts = { footer }
|
|
249
|
+
const finishOpts = { footer, footerDetail }
|
|
194
250
|
if (milestones.length) {
|
|
195
251
|
const stripped = stripMilestones(visibleText)
|
|
196
252
|
if (stripped.trim()) {
|
|
@@ -1204,16 +1204,56 @@ class C2DurableRuntime {
|
|
|
1204
1204
|
// Isolated v2 terminal boundary. The callback is registered before the
|
|
1205
1205
|
// runner is exposed, and only this process can prove which pending receipt
|
|
1206
1206
|
// was actually dispatched. No text or reply context enters the journal.
|
|
1207
|
-
registerTerminalCallback({ receiptId, generation, registeredAt } = {}) {
|
|
1207
|
+
registerTerminalCallback({ receiptId, generation, registeredAt, reuse = false } = {}) {
|
|
1208
1208
|
this.#completionCapacity()
|
|
1209
1209
|
const id = callbackRegistrationId({ receiptId, generation,
|
|
1210
1210
|
domain: 'terminal', kind: 'finalizeEffectCommit' })
|
|
1211
|
+
if (reuse) {
|
|
1212
|
+
const prior = this.#journal.lookupRegistration(id)
|
|
1213
|
+
if (prior) {
|
|
1214
|
+
if (prior.receiptId !== receiptId || prior.generation !== generation ||
|
|
1215
|
+
prior.domain !== 'terminal' || prior.kind !== 'finalizeEffectCommit' ||
|
|
1216
|
+
prior.ordinal !== 0) throw new Error('C2 terminal registration identity differs')
|
|
1217
|
+
return prior
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1211
1220
|
const result = this.#journal.registerCallback({ id, receiptId, generation,
|
|
1212
1221
|
domain: 'terminal', kind: 'finalizeEffectCommit', ordinal: 0, registeredAt })
|
|
1213
1222
|
if (this.#journal.byteLength >= this.#maxJournalBytes) this.#sealForCapacity()
|
|
1214
1223
|
return result
|
|
1215
1224
|
}
|
|
1216
1225
|
|
|
1226
|
+
assertClosedFailureReady({ receiptId, chatId, turnToken, generation,
|
|
1227
|
+
verifyGeneration, assertWriterLock } = {}) {
|
|
1228
|
+
const before = this.#completionCapacity()
|
|
1229
|
+
const witness = this.#activeRuns.get(chatId)
|
|
1230
|
+
const proof = this.#journal.lookupRunnerClosed(
|
|
1231
|
+
`sbot-c2-runner-closed-v1:${receiptId}:${generation}`)
|
|
1232
|
+
const receipt = before.receipts.pending.find(item => item.id === receiptId)
|
|
1233
|
+
const dispatch = proof && this.#journal.effectDescriptor(proof.dispatchEffectId)
|
|
1234
|
+
if (this.blocked || assertWriterLock?.() !== true ||
|
|
1235
|
+
verifyGeneration?.({ receiptId, chatId, turnToken, generation }) !== true ||
|
|
1236
|
+
receipt?.chatId !== chatId || witness?.receiptId !== receiptId ||
|
|
1237
|
+
witness.turnToken !== turnToken || proof?.receiptId !== receiptId ||
|
|
1238
|
+
proof.chatId !== chatId || proof.turnToken !== turnToken ||
|
|
1239
|
+
proof.generation !== generation || dispatch?.type !== 'dispatch' ||
|
|
1240
|
+
dispatch.scope !== chatId || dispatch.receiptId !== receiptId ||
|
|
1241
|
+
dispatch.turnToken !== turnToken ||
|
|
1242
|
+
this.#journal.effectStatus(proof.dispatchEffectId) !== 'acked' ||
|
|
1243
|
+
!this.#journal.hasEvent(`sbot-c2-session-close-v1:${receiptId}:${generation}`) ||
|
|
1244
|
+
before.state.lanes.session?.[chatId]?.closeSeen !== true ||
|
|
1245
|
+
before.state.lanes.turn?.[chatId]?.active !== turnToken ||
|
|
1246
|
+
before.state.lanes.handoff?.[chatId]?.phase !== 'running' ||
|
|
1247
|
+
before.state.lanes.steer?.[chatId]?.entries?.some(item => item.status !== 'landed') ||
|
|
1248
|
+
[...before.effects].some(([id, status]) =>
|
|
1249
|
+
this.#journal.effectDescriptor(id)?.scope === chatId && status !== 'acked') ||
|
|
1250
|
+
!isDeepStrictEqual(this.#coordinator.snapshot(), before.state)) {
|
|
1251
|
+
this.close()
|
|
1252
|
+
throw new Error('C2 failure terminal lacks settled durable proof')
|
|
1253
|
+
}
|
|
1254
|
+
return true
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1217
1257
|
commitTerminalEffect({ receiptId, generation, turnToken, payload } = {}) {
|
|
1218
1258
|
this.#completionCapacity()
|
|
1219
1259
|
if (!RECEIPT_ID.test(receiptId) || !GENERATION.test(generation) ||
|
|
@@ -2200,7 +2240,7 @@ class C2DurableRuntime {
|
|
|
2200
2240
|
// this yet. It executes one externally visible reply only after a durable
|
|
2201
2241
|
// attempt; a lost ACK leaves the receipt and effect ambiguous on restart.
|
|
2202
2242
|
async executeTerminalEffect({ effectId, receiptId, chatId, turnToken, generation,
|
|
2203
|
-
verifyGeneration, resolveTarget, perform, confirm } = {}) {
|
|
2243
|
+
verifyGeneration, resolveTarget, perform, confirm, assertWriterLock = () => true } = {}) {
|
|
2204
2244
|
const before = this.#completionCapacity()
|
|
2205
2245
|
if (typeof effectId !== 'string' || !RECEIPT_ID.test(receiptId) ||
|
|
2206
2246
|
typeof chatId !== 'string' || !chatId || !GENERATION.test(generation) ||
|
|
@@ -2242,7 +2282,8 @@ class C2DurableRuntime {
|
|
|
2242
2282
|
descriptor.receiptId !== receiptId || descriptor.targetId !== null ||
|
|
2243
2283
|
this.#journal.effectStatus(effectId) !== 'pending' ||
|
|
2244
2284
|
registration?.generation !== generation || registration.receiptId !== receiptId ||
|
|
2245
|
-
verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true
|
|
2285
|
+
verifyGeneration({ chatId, receiptId, turnToken, generation }) !== true ||
|
|
2286
|
+
assertWriterLock() !== true) {
|
|
2246
2287
|
throw new Error('C2 terminal effect lacks exact live authority')
|
|
2247
2288
|
}
|
|
2248
2289
|
if (expectedType === 'quietCompletion') {
|
|
@@ -2278,13 +2319,16 @@ class C2DurableRuntime {
|
|
|
2278
2319
|
const reply = await perform({ effectId, target })
|
|
2279
2320
|
confirmed = confirm(reply) === true &&
|
|
2280
2321
|
this.#effectAuthorityReady &&
|
|
2322
|
+
assertWriterLock() === true &&
|
|
2281
2323
|
verifyGeneration({ chatId, receiptId, turnToken, generation }) === true
|
|
2282
2324
|
} catch { /* External outcome is unknown after the durable attempt. */ }
|
|
2283
2325
|
const now = this.#now()
|
|
2284
2326
|
const resolvedAt = Number.isSafeInteger(now) && now > 0
|
|
2285
2327
|
? Math.max(attemptedAt, now) : attemptedAt
|
|
2286
2328
|
if (!confirmed) {
|
|
2287
|
-
|
|
2329
|
+
if (assertWriterLock() === true) {
|
|
2330
|
+
this.#journal.recordEffectOutcome({ effectId, status: 'unknown', resolvedAt })
|
|
2331
|
+
}
|
|
2288
2332
|
this.close()
|
|
2289
2333
|
throw new Error('C2 final reply outcome unknown; manual recovery required')
|
|
2290
2334
|
}
|
|
@@ -147,12 +147,11 @@ function createC2LocalEffectExecutor({ journal, store, resolveTurn,
|
|
|
147
147
|
const chatId = descriptor.scope
|
|
148
148
|
if (effect.type === 'verifyTurn' && effect.success === false) {
|
|
149
149
|
const outcome = journal.snapshot().state.lanes.terminal?.[chatId]?.outcome
|
|
150
|
-
// A
|
|
150
|
+
// A failed task alone says nothing about credential validity. The
|
|
151
151
|
// binary's failed verification intent is acknowledged without writing
|
|
152
152
|
// a false success or altering the previous credential-health record.
|
|
153
|
-
if (outcome?.status !== 'error'
|
|
154
|
-
|
|
155
|
-
throw new Error('C2 local effect payload differs: failed verification lacks a supported quota outcome')
|
|
153
|
+
if (outcome?.status !== 'error') {
|
|
154
|
+
throw new Error('C2 local effect payload differs: failed verification lacks a core error outcome')
|
|
156
155
|
}
|
|
157
156
|
return true
|
|
158
157
|
}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// receive only the v2 decision and stable effect/receipt identities.
|
|
7
7
|
const { C2DurableRuntime } = require('./c2durableruntime')
|
|
8
8
|
const { C2PayloadHandles } = require('./c2payloadhandles')
|
|
9
|
+
const { engineTerminalStep } = require('../agent/privatecore')
|
|
10
|
+
|
|
11
|
+
const FAILURE_NOTICE = '本次任务执行失败。若已执行过工具,请先核对实际结果,再决定是否重试。'
|
|
9
12
|
|
|
10
13
|
const RECEIPT_ID = /^[0-9a-f]{32}$/
|
|
11
14
|
const GENERATION = /^[0-9a-f]{32}$/
|
|
@@ -23,6 +26,14 @@ function terminalReplyText(payload) {
|
|
|
23
26
|
typeof payload.answerText !== 'string') {
|
|
24
27
|
throw new TypeError('C2 terminal reply text fields invalid')
|
|
25
28
|
}
|
|
29
|
+
// The core classifies the result; the host supplies a fixed display string.
|
|
30
|
+
// Neither a provider error nor unfinished assistant text is a final answer.
|
|
31
|
+
if (payload.source === 'result' && payload.isError === true) {
|
|
32
|
+
const event = { ...payload, type: 'finalize' }
|
|
33
|
+
delete event.receiptId; delete event.turnToken; delete event.quiet
|
|
34
|
+
if (engineTerminalStep({ finalized: false, outcome: null }, event)
|
|
35
|
+
.state.outcome.status === 'error') return FAILURE_NOTICE
|
|
36
|
+
}
|
|
26
37
|
return (payload.answerText.trim() ? payload.answerText : payload.bufferText) ||
|
|
27
38
|
payload.resultText
|
|
28
39
|
}
|
|
@@ -106,7 +117,7 @@ function prepareTerminalEffectBridge({ runtime, payloadHandles, receiptId, chatI
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
const effect = { effectId: final.effectId, receiptId, chatId,
|
|
109
|
-
turnToken, generation, verifyGeneration }
|
|
120
|
+
turnToken, generation, verifyGeneration, assertWriterLock }
|
|
110
121
|
if (final.type === 'quietCompletion') {
|
|
111
122
|
if (replyText !== undefined || resolveTarget !== undefined ||
|
|
112
123
|
sendReply !== undefined || confirmReply !== undefined) {
|
package/src/core/v2engine.js
CHANGED
|
@@ -39,6 +39,31 @@ function humanDuration(ms) {
|
|
|
39
39
|
if (minutes < 60) return `${minutes} 分 ${seconds % 60} 秒`
|
|
40
40
|
return `${Math.floor(minutes / 60)} 小时 ${minutes % 60} 分`
|
|
41
41
|
}
|
|
42
|
+
// The final card is an accounting line: keep seconds for short turns and
|
|
43
|
+
// switch to minutes/hours before a long turn becomes an unreadable number.
|
|
44
|
+
function cardDuration(ms) {
|
|
45
|
+
const seconds = Math.max(0, Math.round(ms / 1000))
|
|
46
|
+
if (seconds < 60) return `${seconds} 秒`
|
|
47
|
+
const minutes = Math.floor(seconds / 60)
|
|
48
|
+
if (minutes < 60) return seconds % 60 ? `${minutes} 分 ${seconds % 60} 秒` : `${minutes} 分钟`
|
|
49
|
+
const hours = Math.floor(minutes / 60)
|
|
50
|
+
return minutes % 60 ? `${hours} 小时 ${minutes % 60} 分` : `${hours} 小时`
|
|
51
|
+
}
|
|
52
|
+
function cardModelInfo(group, provider) {
|
|
53
|
+
if (!provider) return { model: '', effort: '' }
|
|
54
|
+
const model = group?.model || provider.model ||
|
|
55
|
+
(provider.runner === 'codex' ? '' : provider.env?.ANTHROPIC_MODEL) || ''
|
|
56
|
+
const effort = provider.runner === 'codex'
|
|
57
|
+
? provider.reasoningEffort : provider.env?.CLAUDE_CODE_EFFORT_LEVEL
|
|
58
|
+
const clean = value => String(value || '').replace(/\s+/g, ' ').trim()
|
|
59
|
+
return { model: clean(model), effort: clean(effort) }
|
|
60
|
+
}
|
|
61
|
+
function cardModelDetail(info, result) {
|
|
62
|
+
// A rejected configured effort may have been retried at the model default.
|
|
63
|
+
const fellBack = result?.effortFellBack || result?.result?.effortFellBack
|
|
64
|
+
return [info?.model, fellBack ? '' : info?.effort]
|
|
65
|
+
.filter(Boolean).join(' ')
|
|
66
|
+
}
|
|
42
67
|
// Group texts name the project by its folder only, never by a machine path.
|
|
43
68
|
function projectName(group) {
|
|
44
69
|
return String(group?.workdir || '').split(/[\\/]/).filter(Boolean).pop() || '(未设)'
|
|
@@ -104,9 +129,11 @@ const T = {
|
|
|
104
129
|
skipPending: '正在跳过当前任务,这条消息没有处理,请稍后重发。',
|
|
105
130
|
quotaFailure: 'Codex 订阅额度已用完,本次任务未完成。请恢复额度后重新发送;本次请求不会自动重试。',
|
|
106
131
|
cardFooter: ({ ms, tokens }) => {
|
|
107
|
-
const parts = [`耗时 ${
|
|
132
|
+
const parts = [`耗时 ${cardDuration(ms)}`]
|
|
108
133
|
const tokenLabel = n => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n)
|
|
109
|
-
if (tokens
|
|
134
|
+
if (tokens && ((tokens.in || 0) + (tokens.cr || 0) + (tokens.cw || 0) + (tokens.out || 0)) > 0) {
|
|
135
|
+
parts.push(`tokens ↑${tokenLabel((tokens.in || 0) + (tokens.cr || 0) + (tokens.cw || 0))} ↓${tokenLabel(tokens.out || 0)}`)
|
|
136
|
+
}
|
|
110
137
|
return parts.join(' · ')
|
|
111
138
|
},
|
|
112
139
|
}
|
|
@@ -136,6 +163,20 @@ function supportedV2QuotaFailure(payload, runnerResult) {
|
|
|
136
163
|
outcome.sideEffects === false
|
|
137
164
|
}
|
|
138
165
|
|
|
166
|
+
// Admit only an observed, closed result on this release's no-retry route.
|
|
167
|
+
// Error classification remains in the core; close/session/effect authority is
|
|
168
|
+
// checked by the terminal path before any delivery can be attempted.
|
|
169
|
+
function supportedV2ClosedFailure(payload, runnerResult) {
|
|
170
|
+
if (!payload || payload.source !== 'result' || payload.aborted ||
|
|
171
|
+
payload.sessionMissing || payload.transientNetwork || payload.hasQuota ||
|
|
172
|
+
payload.freshRetried || payload.resumedSessionCurrent ||
|
|
173
|
+
runnerResult?.aborted !== false) return false
|
|
174
|
+
const terminal = { ...payload, type: 'finalize' }
|
|
175
|
+
delete terminal.receiptId; delete terminal.turnToken; delete terminal.quiet
|
|
176
|
+
return engineTerminalStep({ finalized: false, outcome: null }, terminal)
|
|
177
|
+
.state.outcome.status === 'error'
|
|
178
|
+
}
|
|
179
|
+
|
|
139
180
|
function ordinaryV2Cohort(snapshot, jobs, binding, chatId, receiptId) {
|
|
140
181
|
if (!Array.isArray(jobs) || !jobs.length ||
|
|
141
182
|
jobs.some(job => !job || job.msg?.batch || job.msg?.synthetic ||
|
|
@@ -781,7 +822,9 @@ async _runDurableV2Steer(rt, receiptId) {
|
|
|
781
822
|
invoke: () => control.steerDurable({ msgId: job.msg.msgId, text }) })
|
|
782
823
|
entry.accepted = accepted
|
|
783
824
|
if (accepted) {
|
|
784
|
-
if (entry.landed)
|
|
825
|
+
if (entry.landed) {
|
|
826
|
+
authority.commitSteerObservation({ ...identity, type: 'landed' })
|
|
827
|
+
}
|
|
785
828
|
return { receiptId, steered: true, accepted: true, landed: entry.landed, staged: true }
|
|
786
829
|
}
|
|
787
830
|
if (entry.landed) throw new Error('C2 rejected steer also landed; outcome inconsistent')
|
|
@@ -803,9 +846,20 @@ async _runDurableV2Steer(rt, receiptId) {
|
|
|
803
846
|
return { receiptId, queued: true, staged: true, accepted: false }
|
|
804
847
|
})()
|
|
805
848
|
rt.v2QueueNoticePromise = work
|
|
806
|
-
|
|
849
|
+
let outcome
|
|
850
|
+
try { outcome = await work }
|
|
807
851
|
catch (error) { authority.close(); rt.v2Controller?.abort(); this.durableV2SealCapacity?.(error); throw error }
|
|
808
852
|
finally { if (rt.v2QueueNoticePromise === work) rt.v2QueueNoticePromise = null }
|
|
853
|
+
try {
|
|
854
|
+
if (outcome.accepted && entry.landed) {
|
|
855
|
+
await rt.v2SteerLandedChain
|
|
856
|
+
await rt.v2MoveCard?.(entry)
|
|
857
|
+
}
|
|
858
|
+
return outcome
|
|
859
|
+
} catch (error) {
|
|
860
|
+
authority.close(); rt.v2Controller?.abort(); this.durableV2SealCapacity?.(error)
|
|
861
|
+
throw error
|
|
862
|
+
}
|
|
809
863
|
}
|
|
810
864
|
|
|
811
865
|
async runDurableV2Stop(msg, platform) {
|
|
@@ -1203,7 +1257,8 @@ async _drainDurableV2NormalChain(initial) {
|
|
|
1203
1257
|
const outcome = engineTerminalStep({ finalized: false, outcome: null },
|
|
1204
1258
|
terminal).state.outcome
|
|
1205
1259
|
const quotaFailure = supportedV2QuotaFailure(payload, rt.v2RunnerResult)
|
|
1206
|
-
|
|
1260
|
+
const closedFailure = supportedV2ClosedFailure(payload, rt.v2RunnerResult)
|
|
1261
|
+
if (['error', 'empty', 'stopped'].includes(outcome.status) && !quotaFailure && !closedFailure) {
|
|
1207
1262
|
const classified = this.classifyDurableV2UnresolvedTurn({ ...identity,
|
|
1208
1263
|
payload })
|
|
1209
1264
|
const error = new Error('C2 v2 ordinary result requires fault recovery')
|
|
@@ -1214,7 +1269,7 @@ async _drainDurableV2NormalChain(initial) {
|
|
|
1214
1269
|
if (payload.source !== 'result' || payload.sessionMissing ||
|
|
1215
1270
|
(payload.hasQuota && !quotaFailure) || payload.transientNetwork || payload.freshRetried ||
|
|
1216
1271
|
payload.resumedSessionCurrent ||
|
|
1217
|
-
(!['ok', 'no-reply'].includes(outcome.status) && !quotaFailure)) {
|
|
1272
|
+
(!['ok', 'no-reply'].includes(outcome.status) && !quotaFailure && !closedFailure)) {
|
|
1218
1273
|
authority.close()
|
|
1219
1274
|
const error = new Error('C2 v2 ordinary result needs a retry or notice decision')
|
|
1220
1275
|
error.code = 'C2_V2_UNRESOLVED_TURN'
|
|
@@ -1232,7 +1287,7 @@ async _drainDurableV2NormalChain(initial) {
|
|
|
1232
1287
|
for (const settledReceiptId of completed.settledReceiptIds ||
|
|
1233
1288
|
[identity.receiptId]) {
|
|
1234
1289
|
settled.push({ receiptId: settledReceiptId,
|
|
1235
|
-
status: quotaFailure ? 'error' : silent ? 'silent' : 'answered',
|
|
1290
|
+
status: quotaFailure || closedFailure ? 'error' : silent ? 'silent' : 'answered',
|
|
1236
1291
|
terminalEffectId: completed.terminalEffectId })
|
|
1237
1292
|
}
|
|
1238
1293
|
if (authority.blocked) {
|
|
@@ -1436,7 +1491,12 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1436
1491
|
rt.v2Doing = []
|
|
1437
1492
|
rt.v2AutoMilestones = []
|
|
1438
1493
|
rt.v2CardQuotaFailure = false
|
|
1494
|
+
rt.v2CardModelInfo = null
|
|
1439
1495
|
rt.v2CardBinding = null
|
|
1496
|
+
rt.v2CardAnchor = null
|
|
1497
|
+
rt.v2CardJoinedNames = []
|
|
1498
|
+
rt.v2LastCardText = ''
|
|
1499
|
+
rt.v2MoveCard = null
|
|
1440
1500
|
rt.v2StreamRequested = false
|
|
1441
1501
|
rt.v2StreamQuiescing = false
|
|
1442
1502
|
rt.v2StreamChain = Promise.resolve()
|
|
@@ -1472,6 +1532,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1472
1532
|
if (!provider || (provider.baseUrl && !provider.apiKey)) {
|
|
1473
1533
|
throw new Error('C2 v2 runner provider unavailable')
|
|
1474
1534
|
}
|
|
1535
|
+
rt.v2CardModelInfo = cardModelInfo(job.group, provider)
|
|
1475
1536
|
rt.v2SteerControl = null
|
|
1476
1537
|
rt.v2SteerEntries = new Map()
|
|
1477
1538
|
rt.v2SteerClosing = false
|
|
@@ -1516,6 +1577,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1516
1577
|
if (rt.v2CardBinding?.opened) await rt.v2CardBinding.path.flushForTerminal()
|
|
1517
1578
|
if (rt.v2RunnerGeneration !== generation) return
|
|
1518
1579
|
authority.commitSteerObservation({ ...steerIdentity, receiptId: entry.identity.receiptId, type: 'landed' })
|
|
1580
|
+
await rt.v2MoveCard?.(entry)
|
|
1519
1581
|
})
|
|
1520
1582
|
rt.v2SteerLandedChain = observation
|
|
1521
1583
|
observation.catch(() => { authority.close(); controller?.abort() })
|
|
@@ -1531,6 +1593,8 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1531
1593
|
rt.v2RunnerNotStarted = false
|
|
1532
1594
|
let streamChain = Promise.resolve()
|
|
1533
1595
|
let streamFailure = null
|
|
1596
|
+
const joinedLine = () => rt.v2CardJoinedNames.length
|
|
1597
|
+
? `\n\n↪ 已加入 ${rt.v2CardJoinedNames.join('、')} 的补充` : ''
|
|
1534
1598
|
// Card progress (same mechanism and wording as the legacy engine): deterministic
|
|
1535
1599
|
// milestones and "recently doing" from tool calls, an early "received" card
|
|
1536
1600
|
// and a pulse line. All of it is process-local card text; the card itself is
|
|
@@ -1547,6 +1611,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1547
1611
|
rt.v2TerminalStarted || controller.signal.aborted || rt.v2CancelControl ||
|
|
1548
1612
|
rt.v2StreamQuiescing || !!authority.scopeState('stopIntent', chatId)
|
|
1549
1613
|
const enqueueCardText = text => {
|
|
1614
|
+
rt.v2LastCardText = text
|
|
1550
1615
|
rt.v2StreamRequested = true
|
|
1551
1616
|
streamChain = streamChain.then(async () => {
|
|
1552
1617
|
if (rt.v2QueueNoticePromise) await rt.v2QueueNoticePromise
|
|
@@ -1558,7 +1623,8 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1558
1623
|
if (!rt.v2CardBinding) {
|
|
1559
1624
|
const path = prepareCardTerminalPath({ runtime: authority,
|
|
1560
1625
|
payloadHandles: this.durableV2Payloads, platform: job.platform,
|
|
1561
|
-
replyCtx: job.msg.replyCtx,
|
|
1626
|
+
replyCtx: job.msg.replyCtx, cardAnchor: rt.v2CardAnchor,
|
|
1627
|
+
receiptId, chatId, turnToken,
|
|
1562
1628
|
generation, verifyGeneration,
|
|
1563
1629
|
applyLocalEffect: this.durableV2ApplyLocalEffect,
|
|
1564
1630
|
assertWriterLock: this.durableV2AssertWriterLock,
|
|
@@ -1573,7 +1639,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1573
1639
|
rt.v2CardBinding = { path, receiptId, turnToken,
|
|
1574
1640
|
generation, opened }
|
|
1575
1641
|
}
|
|
1576
|
-
if (rt.v2CardBinding.opened) rt.v2CardBinding.path.update(text)
|
|
1642
|
+
if (rt.v2CardBinding.opened) rt.v2CardBinding.path.update(text + joinedLine())
|
|
1577
1643
|
})
|
|
1578
1644
|
rt.v2StreamChain = streamChain
|
|
1579
1645
|
streamChain.catch(error => {
|
|
@@ -1581,6 +1647,33 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1581
1647
|
try { controller.abort() } catch {}
|
|
1582
1648
|
})
|
|
1583
1649
|
}
|
|
1650
|
+
rt.v2MoveCard = entry => {
|
|
1651
|
+
if (entry.movePromise) return entry.movePromise
|
|
1652
|
+
const move = streamChain.then(async () => {
|
|
1653
|
+
if (progressClosed() || job.platform.features?.cardPinned !== true) return false
|
|
1654
|
+
const anchor = { receiptId: entry.identity.receiptId,
|
|
1655
|
+
replyCtx: entry.job.msg.replyCtx }
|
|
1656
|
+
const who = entry.job.msg.senderName || entry.job.msg.userId || 'user'
|
|
1657
|
+
if (!rt.v2CardJoinedNames.includes(who)) rt.v2CardJoinedNames.push(who)
|
|
1658
|
+
if (!rt.v2CardBinding) {
|
|
1659
|
+
rt.v2CardAnchor = anchor
|
|
1660
|
+
return true
|
|
1661
|
+
}
|
|
1662
|
+
if (!rt.v2CardBinding.opened) return false
|
|
1663
|
+
rt.v2CardBinding.path.update((rt.v2LastCardText || progress.T.ackWorking) + joinedLine())
|
|
1664
|
+
const moved = await rt.v2CardBinding.path.relocate(anchor)
|
|
1665
|
+
if (moved) rt.v2CardAnchor = anchor
|
|
1666
|
+
return moved
|
|
1667
|
+
})
|
|
1668
|
+
streamChain = move
|
|
1669
|
+
rt.v2StreamChain = move
|
|
1670
|
+
entry.movePromise = move
|
|
1671
|
+
move.catch(error => {
|
|
1672
|
+
streamFailure = error
|
|
1673
|
+
try { controller.abort() } catch {}
|
|
1674
|
+
})
|
|
1675
|
+
return move
|
|
1676
|
+
}
|
|
1584
1677
|
const enqueueFrame = event => {
|
|
1585
1678
|
if (event.type !== 'assistant') return
|
|
1586
1679
|
// /status and the pulse read these; a late event from an older
|
|
@@ -1650,10 +1743,8 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1650
1743
|
const sofar = buffer.trim()
|
|
1651
1744
|
const body = autoBlock() + (sofar && !NO_REPLY.startsWith(sofar)
|
|
1652
1745
|
? progress.renderMilestones(buffer) + '\n\n' : '')
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
progress.humanDur(Date.now() - pulseFrom), rt.v2Doing, stale))
|
|
1656
|
-
} catch { /* a closed card never affects the turn */ }
|
|
1746
|
+
enqueueCardText(body + progress.T.pulse(
|
|
1747
|
+
progress.humanDur(Date.now() - pulseFrom), rt.v2Doing, stale))
|
|
1657
1748
|
}, PULSE_MS)
|
|
1658
1749
|
if (pulseTimer.unref) pulseTimer.unref()
|
|
1659
1750
|
}
|
|
@@ -1872,6 +1963,7 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1872
1963
|
const projected = rt?.v2TerminalProjection
|
|
1873
1964
|
const resultText = projected?.payload?.resultText
|
|
1874
1965
|
const quotaFailure = supportedV2QuotaFailure(payload, rt?.v2RunnerResult)
|
|
1966
|
+
const closedFailure = supportedV2ClosedFailure(payload, rt?.v2RunnerResult)
|
|
1875
1967
|
if (!/^[0-9a-f]{32}$/.test(receiptId || '') ||
|
|
1876
1968
|
!Number.isSafeInteger(turnToken) || turnToken <= 0 ||
|
|
1877
1969
|
typeof effectId !== 'string' || !effectId ||
|
|
@@ -1903,10 +1995,9 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1903
1995
|
(!isDeepStrictEqual(payload.batchReceiptIds, cohort.receiptIds) ||
|
|
1904
1996
|
payload.replyTargetReceiptId !== receiptId)) ||
|
|
1905
1997
|
!isDeepStrictEqual(payload, projected.payload) ||
|
|
1906
|
-
//
|
|
1907
|
-
//
|
|
1908
|
-
|
|
1909
|
-
payload.source !== 'result' || payload.aborted || (payload.isError && !quotaFailure) ||
|
|
1998
|
+
// A deterministic failure is delivered once, after the same durable
|
|
1999
|
+
// close checks as success. Retry and unknown outcomes stay sealed.
|
|
2000
|
+
payload.source !== 'result' || payload.aborted || (payload.isError && !quotaFailure && !closedFailure) ||
|
|
1910
2001
|
payload.sessionMissing || (payload.hasQuota && !quotaFailure) || payload.transientNetwork ||
|
|
1911
2002
|
payload.freshRetried || payload.resumedSessionCurrent ||
|
|
1912
2003
|
(replyText !== undefined && replyText !== terminalReplyText(projected.payload)) ||
|
|
@@ -1920,6 +2011,9 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1920
2011
|
value.generation === generation && rt.v2RunnerGeneration === generation
|
|
1921
2012
|
try {
|
|
1922
2013
|
const preopened = rt.v2CardBinding
|
|
2014
|
+
if (closedFailure) authority.assertClosedFailureReady({ receiptId, chatId,
|
|
2015
|
+
turnToken, generation, verifyGeneration,
|
|
2016
|
+
assertWriterLock: this.durableV2AssertWriterLock })
|
|
1923
2017
|
if (preopened && (preopened.receiptId !== receiptId ||
|
|
1924
2018
|
preopened.turnToken !== turnToken ||
|
|
1925
2019
|
preopened.generation !== generation)) {
|
|
@@ -1927,7 +2021,8 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1927
2021
|
}
|
|
1928
2022
|
const path = preopened?.path || prepareCardTerminalPath({ runtime: authority,
|
|
1929
2023
|
payloadHandles: handles, platform: job.platform,
|
|
1930
|
-
replyCtx: job.msg.replyCtx,
|
|
2024
|
+
replyCtx: job.msg.replyCtx, cardAnchor: rt.v2CardAnchor, preferPlain: closedFailure,
|
|
2025
|
+
receiptId, chatId, turnToken, generation,
|
|
1931
2026
|
verifyGeneration, applyLocalEffect: this.durableV2ApplyLocalEffect,
|
|
1932
2027
|
assertWriterLock: this.durableV2AssertWriterLock,
|
|
1933
2028
|
redactText: text => redact(quotaFailure
|
|
@@ -1942,6 +2037,8 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1942
2037
|
for (const text of updates) path.update(text)
|
|
1943
2038
|
const footer = T.cardFooter({ ms: Math.max(0, Date.now() - rt.v2StartedAt),
|
|
1944
2039
|
tokens: rt.v2UsageDecision?.tokens || null })
|
|
2040
|
+
const footerDetail = job.platform.features?.cardQuota === true
|
|
2041
|
+
? cardModelDetail(rt.v2CardModelInfo, rt.v2RunnerResult) : ''
|
|
1945
2042
|
// Milestones from tool calls first, then the model's own `>> ` lines;
|
|
1946
2043
|
// the card shows them in its "process" fold (display only).
|
|
1947
2044
|
rt.v2CardQuotaFailure = !!quotaFailure
|
|
@@ -1950,7 +2047,8 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1950
2047
|
...progress.milestonesOf(payload.bufferText)]
|
|
1951
2048
|
.filter((m, i, a) => a.indexOf(m) === i).map(redactMilestone)
|
|
1952
2049
|
.filter(m => typeof m === 'string' && m.trim())
|
|
1953
|
-
const completed = await path.complete({ payload, replyText, paused,
|
|
2050
|
+
const completed = await path.complete({ payload, replyText, paused,
|
|
2051
|
+
footer, footerDetail, milestones })
|
|
1954
2052
|
const after = authority.snapshot()
|
|
1955
2053
|
const queue = after.state?.lanes?.queue?.[chatId]?.queue
|
|
1956
2054
|
let successor = null
|
|
@@ -2064,7 +2162,7 @@ classifyDurableV2UnresolvedTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
2064
2162
|
}
|
|
2065
2163
|
rt.v2TerminalStarted = true
|
|
2066
2164
|
authority.registerTerminalCallback({ receiptId, generation,
|
|
2067
|
-
registeredAt: Date.now() })
|
|
2165
|
+
registeredAt: Date.now(), reuse: true })
|
|
2068
2166
|
return authority.commitUnresolvedClassification({ receiptId, generation,
|
|
2069
2167
|
turnToken, status, payload,
|
|
2070
2168
|
verifyGeneration: value => value.chatId === chatId &&
|
package/src/platform/feishu.js
CHANGED
|
@@ -605,7 +605,7 @@ function createFeishuPlatform({ appId, appSecret, botOpenId = null, onlyWhenMent
|
|
|
605
605
|
}
|
|
606
606
|
|
|
607
607
|
return {
|
|
608
|
-
features: { cardQuota: true, noticeCard: true }, //
|
|
608
|
+
features: { cardQuota: true, noticeCard: true, cardPinned: true }, // 回复卡钉在触发消息下;补话落地后可搬到新消息下
|
|
609
609
|
async start(onMessage, hooks = {}) {
|
|
610
610
|
if (!botOpenId && !onlyWhenMentioned) log('warning: feishu.botOpenId not set — cannot tell @bot from @others; all group messages will be handled')
|
|
611
611
|
if (hooks.onFatal !== undefined && typeof hooks.onFatal !== 'function') {
|