sintbot 1.4.8 → 1.4.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/CHANGELOG.en.json +30 -313
- package/CHANGELOG.json +30 -320
- 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/claudecode.js +4 -0
- package/src/agent/codexapp.js +6 -0
- package/src/agent/codexcode.js +4 -0
- package/src/agent/privatecore.js +1 -1
- package/src/agent/privatecore.wasm +0 -0
- package/src/core/c2cardterminalpath.js +69 -11
- package/src/core/c2durableruntime.js +48 -4
- package/src/core/c2localeffectexecutor.js +3 -4
- package/src/core/c2terminaleffectbridge.js +14 -1
- package/src/core/c2turnprojection.js +9 -6
- package/src/core/v2engine.js +121 -22
- package/src/platform/feishu.js +1 -1
|
@@ -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,10 @@
|
|
|
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 = '本次任务执行失败。若已执行过工具,请先核对实际结果,再决定是否重试。'
|
|
12
|
+
const INTERRUPTED_NOTICE = '本次任务执行中断,未收到最终结果。已执行的操作可能已经生效,请核对后再决定是否重试。'
|
|
9
13
|
|
|
10
14
|
const RECEIPT_ID = /^[0-9a-f]{32}$/
|
|
11
15
|
const GENERATION = /^[0-9a-f]{32}$/
|
|
@@ -23,6 +27,15 @@ function terminalReplyText(payload) {
|
|
|
23
27
|
typeof payload.answerText !== 'string') {
|
|
24
28
|
throw new TypeError('C2 terminal reply text fields invalid')
|
|
25
29
|
}
|
|
30
|
+
// The core classifies the result; the host supplies a fixed display string.
|
|
31
|
+
// Neither a provider error nor unfinished assistant text is a final answer.
|
|
32
|
+
if (payload.source === 'runner-exit' || payload.source === 'result' && payload.isError === true) {
|
|
33
|
+
const event = { ...payload, type: 'finalize' }
|
|
34
|
+
delete event.receiptId; delete event.turnToken; delete event.quiet
|
|
35
|
+
if (engineTerminalStep({ finalized: false, outcome: null }, event)
|
|
36
|
+
.state.outcome.status === 'error') return payload.source === 'runner-exit'
|
|
37
|
+
? INTERRUPTED_NOTICE : FAILURE_NOTICE
|
|
38
|
+
}
|
|
26
39
|
return (payload.answerText.trim() ? payload.answerText : payload.bufferText) ||
|
|
27
40
|
payload.resultText
|
|
28
41
|
}
|
|
@@ -106,7 +119,7 @@ function prepareTerminalEffectBridge({ runtime, payloadHandles, receiptId, chatI
|
|
|
106
119
|
}
|
|
107
120
|
}
|
|
108
121
|
const effect = { effectId: final.effectId, receiptId, chatId,
|
|
109
|
-
turnToken, generation, verifyGeneration }
|
|
122
|
+
turnToken, generation, verifyGeneration, assertWriterLock }
|
|
110
123
|
if (final.type === 'quietCompletion') {
|
|
111
124
|
if (replyText !== undefined || resolveTarget !== undefined ||
|
|
112
125
|
sendReply !== undefined || confirmReply !== undefined) {
|
|
@@ -8,11 +8,11 @@ const NO_REPLY = 'NO_REPLY'
|
|
|
8
8
|
const { isDeepStrictEqual } = require('node:util')
|
|
9
9
|
|
|
10
10
|
function createC2TurnProjection({ receiptId, turnToken, quiet, resumed = false,
|
|
11
|
-
freshRetried = false, batchReceiptIds } = {}) {
|
|
11
|
+
freshRetried = false, batchReceiptIds, requireResult = false } = {}) {
|
|
12
12
|
if (!RECEIPT_ID.test(receiptId || '') ||
|
|
13
13
|
!Number.isSafeInteger(turnToken) || turnToken <= 0 ||
|
|
14
14
|
typeof quiet !== 'boolean' || typeof resumed !== 'boolean' ||
|
|
15
|
-
typeof freshRetried !== 'boolean') {
|
|
15
|
+
typeof freshRetried !== 'boolean' || typeof requireResult !== 'boolean') {
|
|
16
16
|
throw new TypeError('C2 projection needs a typed running receipt')
|
|
17
17
|
}
|
|
18
18
|
if (batchReceiptIds !== undefined &&
|
|
@@ -137,10 +137,11 @@ function createC2TurnProjection({ receiptId, turnToken, quiet, resumed = false,
|
|
|
137
137
|
throw new TypeError('C2 projection current resumed session requires abort')
|
|
138
138
|
}
|
|
139
139
|
completed = true
|
|
140
|
+
const runnerExit = requireResult && !observedResult && !runnerResult.aborted
|
|
140
141
|
const resultText = observedResult?.text || ''
|
|
141
142
|
const payload = Object.freeze({
|
|
142
143
|
type: 'finalizeEffectCommit', receiptId, turnToken, quiet,
|
|
143
|
-
source: observedResult ? 'result' : 'close',
|
|
144
|
+
source: observedResult ? 'result' : runnerExit ? 'runner-exit' : 'close',
|
|
144
145
|
aborted: runnerResult.aborted,
|
|
145
146
|
isError: observedResult?.isError || false,
|
|
146
147
|
resumed, sessionMissing: observedResult?.sessionMissing || false,
|
|
@@ -156,10 +157,12 @@ function createC2TurnProjection({ receiptId, turnToken, quiet, resumed = false,
|
|
|
156
157
|
const display = Object.freeze({
|
|
157
158
|
resultText, bufferText, answerText, fullText,
|
|
158
159
|
finalAnswer: (answerText.trim() ? answerText : bufferText) || resultText,
|
|
159
|
-
silent: quiet || resultText.trim() === NO_REPLY ||
|
|
160
|
-
(!resultText.trim() && bufferText.trim() === NO_REPLY),
|
|
160
|
+
silent: quiet || !runnerExit && (resultText.trim() === NO_REPLY ||
|
|
161
|
+
(!resultText.trim() && bufferText.trim() === NO_REPLY)),
|
|
161
162
|
assistantEvents, tools,
|
|
162
|
-
|
|
163
|
+
// A crashed process may never have flushed its new session. Retain the
|
|
164
|
+
// previously stored session; never publish an unproven new session ID.
|
|
165
|
+
sessionId: runnerExit ? null : runnerResult.sessionId || sessionId,
|
|
163
166
|
contextTokens,
|
|
164
167
|
usage: result?.usage || null,
|
|
165
168
|
usageCumulative: result?.usageCumulative === true,
|
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,21 @@ 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 || !['result', 'runner-exit'].includes(payload.source) || payload.aborted ||
|
|
171
|
+
payload.sessionMissing || payload.transientNetwork || payload.hasQuota ||
|
|
172
|
+
payload.freshRetried || payload.resumedSessionCurrent ||
|
|
173
|
+
runnerResult?.aborted !== false ||
|
|
174
|
+
(payload.source === 'runner-exit' && runnerResult.result != null)) return false
|
|
175
|
+
const terminal = { ...payload, type: 'finalize' }
|
|
176
|
+
delete terminal.receiptId; delete terminal.turnToken; delete terminal.quiet
|
|
177
|
+
return engineTerminalStep({ finalized: false, outcome: null }, terminal)
|
|
178
|
+
.state.outcome.status === 'error'
|
|
179
|
+
}
|
|
180
|
+
|
|
139
181
|
function ordinaryV2Cohort(snapshot, jobs, binding, chatId, receiptId) {
|
|
140
182
|
if (!Array.isArray(jobs) || !jobs.length ||
|
|
141
183
|
jobs.some(job => !job || job.msg?.batch || job.msg?.synthetic ||
|
|
@@ -781,7 +823,9 @@ async _runDurableV2Steer(rt, receiptId) {
|
|
|
781
823
|
invoke: () => control.steerDurable({ msgId: job.msg.msgId, text }) })
|
|
782
824
|
entry.accepted = accepted
|
|
783
825
|
if (accepted) {
|
|
784
|
-
if (entry.landed)
|
|
826
|
+
if (entry.landed) {
|
|
827
|
+
authority.commitSteerObservation({ ...identity, type: 'landed' })
|
|
828
|
+
}
|
|
785
829
|
return { receiptId, steered: true, accepted: true, landed: entry.landed, staged: true }
|
|
786
830
|
}
|
|
787
831
|
if (entry.landed) throw new Error('C2 rejected steer also landed; outcome inconsistent')
|
|
@@ -803,9 +847,20 @@ async _runDurableV2Steer(rt, receiptId) {
|
|
|
803
847
|
return { receiptId, queued: true, staged: true, accepted: false }
|
|
804
848
|
})()
|
|
805
849
|
rt.v2QueueNoticePromise = work
|
|
806
|
-
|
|
850
|
+
let outcome
|
|
851
|
+
try { outcome = await work }
|
|
807
852
|
catch (error) { authority.close(); rt.v2Controller?.abort(); this.durableV2SealCapacity?.(error); throw error }
|
|
808
853
|
finally { if (rt.v2QueueNoticePromise === work) rt.v2QueueNoticePromise = null }
|
|
854
|
+
try {
|
|
855
|
+
if (outcome.accepted && entry.landed) {
|
|
856
|
+
await rt.v2SteerLandedChain
|
|
857
|
+
await rt.v2MoveCard?.(entry)
|
|
858
|
+
}
|
|
859
|
+
return outcome
|
|
860
|
+
} catch (error) {
|
|
861
|
+
authority.close(); rt.v2Controller?.abort(); this.durableV2SealCapacity?.(error)
|
|
862
|
+
throw error
|
|
863
|
+
}
|
|
809
864
|
}
|
|
810
865
|
|
|
811
866
|
async runDurableV2Stop(msg, platform) {
|
|
@@ -1203,7 +1258,8 @@ async _drainDurableV2NormalChain(initial) {
|
|
|
1203
1258
|
const outcome = engineTerminalStep({ finalized: false, outcome: null },
|
|
1204
1259
|
terminal).state.outcome
|
|
1205
1260
|
const quotaFailure = supportedV2QuotaFailure(payload, rt.v2RunnerResult)
|
|
1206
|
-
|
|
1261
|
+
const closedFailure = supportedV2ClosedFailure(payload, rt.v2RunnerResult)
|
|
1262
|
+
if (['error', 'empty', 'stopped'].includes(outcome.status) && !quotaFailure && !closedFailure) {
|
|
1207
1263
|
const classified = this.classifyDurableV2UnresolvedTurn({ ...identity,
|
|
1208
1264
|
payload })
|
|
1209
1265
|
const error = new Error('C2 v2 ordinary result requires fault recovery')
|
|
@@ -1211,10 +1267,10 @@ async _drainDurableV2NormalChain(initial) {
|
|
|
1211
1267
|
error.classificationEventId = classified.eventId
|
|
1212
1268
|
throw error
|
|
1213
1269
|
}
|
|
1214
|
-
if (payload.source !== 'result' || payload.sessionMissing ||
|
|
1270
|
+
if ((payload.source !== 'result' && !closedFailure) || payload.sessionMissing ||
|
|
1215
1271
|
(payload.hasQuota && !quotaFailure) || payload.transientNetwork || payload.freshRetried ||
|
|
1216
1272
|
payload.resumedSessionCurrent ||
|
|
1217
|
-
(!['ok', 'no-reply'].includes(outcome.status) && !quotaFailure)) {
|
|
1273
|
+
(!['ok', 'no-reply'].includes(outcome.status) && !quotaFailure && !closedFailure)) {
|
|
1218
1274
|
authority.close()
|
|
1219
1275
|
const error = new Error('C2 v2 ordinary result needs a retry or notice decision')
|
|
1220
1276
|
error.code = 'C2_V2_UNRESOLVED_TURN'
|
|
@@ -1232,7 +1288,7 @@ async _drainDurableV2NormalChain(initial) {
|
|
|
1232
1288
|
for (const settledReceiptId of completed.settledReceiptIds ||
|
|
1233
1289
|
[identity.receiptId]) {
|
|
1234
1290
|
settled.push({ receiptId: settledReceiptId,
|
|
1235
|
-
status: quotaFailure ? 'error' : silent ? 'silent' : 'answered',
|
|
1291
|
+
status: quotaFailure || closedFailure ? 'error' : silent ? 'silent' : 'answered',
|
|
1236
1292
|
terminalEffectId: completed.terminalEffectId })
|
|
1237
1293
|
}
|
|
1238
1294
|
if (authority.blocked) {
|
|
@@ -1436,7 +1492,12 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1436
1492
|
rt.v2Doing = []
|
|
1437
1493
|
rt.v2AutoMilestones = []
|
|
1438
1494
|
rt.v2CardQuotaFailure = false
|
|
1495
|
+
rt.v2CardModelInfo = null
|
|
1439
1496
|
rt.v2CardBinding = null
|
|
1497
|
+
rt.v2CardAnchor = null
|
|
1498
|
+
rt.v2CardJoinedNames = []
|
|
1499
|
+
rt.v2LastCardText = ''
|
|
1500
|
+
rt.v2MoveCard = null
|
|
1440
1501
|
rt.v2StreamRequested = false
|
|
1441
1502
|
rt.v2StreamQuiescing = false
|
|
1442
1503
|
rt.v2StreamChain = Promise.resolve()
|
|
@@ -1463,7 +1524,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1463
1524
|
throw new Error('C2 v2 runner effect changed before start')
|
|
1464
1525
|
}
|
|
1465
1526
|
const projection = createC2TurnProjection({ receiptId, turnToken,
|
|
1466
|
-
quiet: receipt.quiet,
|
|
1527
|
+
quiet: receipt.quiet, requireResult: true,
|
|
1467
1528
|
resumed: !!this.store.getSession(chatId), freshRetried: false,
|
|
1468
1529
|
...(cohort.receiptIds.length > 1
|
|
1469
1530
|
? { batchReceiptIds: cohort.receiptIds } : {}) })
|
|
@@ -1472,6 +1533,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1472
1533
|
if (!provider || (provider.baseUrl && !provider.apiKey)) {
|
|
1473
1534
|
throw new Error('C2 v2 runner provider unavailable')
|
|
1474
1535
|
}
|
|
1536
|
+
rt.v2CardModelInfo = cardModelInfo(job.group, provider)
|
|
1475
1537
|
rt.v2SteerControl = null
|
|
1476
1538
|
rt.v2SteerEntries = new Map()
|
|
1477
1539
|
rt.v2SteerClosing = false
|
|
@@ -1516,6 +1578,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1516
1578
|
if (rt.v2CardBinding?.opened) await rt.v2CardBinding.path.flushForTerminal()
|
|
1517
1579
|
if (rt.v2RunnerGeneration !== generation) return
|
|
1518
1580
|
authority.commitSteerObservation({ ...steerIdentity, receiptId: entry.identity.receiptId, type: 'landed' })
|
|
1581
|
+
await rt.v2MoveCard?.(entry)
|
|
1519
1582
|
})
|
|
1520
1583
|
rt.v2SteerLandedChain = observation
|
|
1521
1584
|
observation.catch(() => { authority.close(); controller?.abort() })
|
|
@@ -1531,6 +1594,8 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1531
1594
|
rt.v2RunnerNotStarted = false
|
|
1532
1595
|
let streamChain = Promise.resolve()
|
|
1533
1596
|
let streamFailure = null
|
|
1597
|
+
const joinedLine = () => rt.v2CardJoinedNames.length
|
|
1598
|
+
? `\n\n↪ 已加入 ${rt.v2CardJoinedNames.join('、')} 的补充` : ''
|
|
1534
1599
|
// Card progress (same mechanism and wording as the legacy engine): deterministic
|
|
1535
1600
|
// milestones and "recently doing" from tool calls, an early "received" card
|
|
1536
1601
|
// and a pulse line. All of it is process-local card text; the card itself is
|
|
@@ -1547,6 +1612,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1547
1612
|
rt.v2TerminalStarted || controller.signal.aborted || rt.v2CancelControl ||
|
|
1548
1613
|
rt.v2StreamQuiescing || !!authority.scopeState('stopIntent', chatId)
|
|
1549
1614
|
const enqueueCardText = text => {
|
|
1615
|
+
rt.v2LastCardText = text
|
|
1550
1616
|
rt.v2StreamRequested = true
|
|
1551
1617
|
streamChain = streamChain.then(async () => {
|
|
1552
1618
|
if (rt.v2QueueNoticePromise) await rt.v2QueueNoticePromise
|
|
@@ -1558,7 +1624,8 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1558
1624
|
if (!rt.v2CardBinding) {
|
|
1559
1625
|
const path = prepareCardTerminalPath({ runtime: authority,
|
|
1560
1626
|
payloadHandles: this.durableV2Payloads, platform: job.platform,
|
|
1561
|
-
replyCtx: job.msg.replyCtx,
|
|
1627
|
+
replyCtx: job.msg.replyCtx, cardAnchor: rt.v2CardAnchor,
|
|
1628
|
+
receiptId, chatId, turnToken,
|
|
1562
1629
|
generation, verifyGeneration,
|
|
1563
1630
|
applyLocalEffect: this.durableV2ApplyLocalEffect,
|
|
1564
1631
|
assertWriterLock: this.durableV2AssertWriterLock,
|
|
@@ -1573,7 +1640,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1573
1640
|
rt.v2CardBinding = { path, receiptId, turnToken,
|
|
1574
1641
|
generation, opened }
|
|
1575
1642
|
}
|
|
1576
|
-
if (rt.v2CardBinding.opened) rt.v2CardBinding.path.update(text)
|
|
1643
|
+
if (rt.v2CardBinding.opened) rt.v2CardBinding.path.update(text + joinedLine())
|
|
1577
1644
|
})
|
|
1578
1645
|
rt.v2StreamChain = streamChain
|
|
1579
1646
|
streamChain.catch(error => {
|
|
@@ -1581,6 +1648,33 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1581
1648
|
try { controller.abort() } catch {}
|
|
1582
1649
|
})
|
|
1583
1650
|
}
|
|
1651
|
+
rt.v2MoveCard = entry => {
|
|
1652
|
+
if (entry.movePromise) return entry.movePromise
|
|
1653
|
+
const move = streamChain.then(async () => {
|
|
1654
|
+
if (progressClosed() || job.platform.features?.cardPinned !== true) return false
|
|
1655
|
+
const anchor = { receiptId: entry.identity.receiptId,
|
|
1656
|
+
replyCtx: entry.job.msg.replyCtx }
|
|
1657
|
+
const who = entry.job.msg.senderName || entry.job.msg.userId || 'user'
|
|
1658
|
+
if (!rt.v2CardJoinedNames.includes(who)) rt.v2CardJoinedNames.push(who)
|
|
1659
|
+
if (!rt.v2CardBinding) {
|
|
1660
|
+
rt.v2CardAnchor = anchor
|
|
1661
|
+
return true
|
|
1662
|
+
}
|
|
1663
|
+
if (!rt.v2CardBinding.opened) return false
|
|
1664
|
+
rt.v2CardBinding.path.update((rt.v2LastCardText || progress.T.ackWorking) + joinedLine())
|
|
1665
|
+
const moved = await rt.v2CardBinding.path.relocate(anchor)
|
|
1666
|
+
if (moved) rt.v2CardAnchor = anchor
|
|
1667
|
+
return moved
|
|
1668
|
+
})
|
|
1669
|
+
streamChain = move
|
|
1670
|
+
rt.v2StreamChain = move
|
|
1671
|
+
entry.movePromise = move
|
|
1672
|
+
move.catch(error => {
|
|
1673
|
+
streamFailure = error
|
|
1674
|
+
try { controller.abort() } catch {}
|
|
1675
|
+
})
|
|
1676
|
+
return move
|
|
1677
|
+
}
|
|
1584
1678
|
const enqueueFrame = event => {
|
|
1585
1679
|
if (event.type !== 'assistant') return
|
|
1586
1680
|
// /status and the pulse read these; a late event from an older
|
|
@@ -1650,10 +1744,8 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
|
|
|
1650
1744
|
const sofar = buffer.trim()
|
|
1651
1745
|
const body = autoBlock() + (sofar && !NO_REPLY.startsWith(sofar)
|
|
1652
1746
|
? 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 */ }
|
|
1747
|
+
enqueueCardText(body + progress.T.pulse(
|
|
1748
|
+
progress.humanDur(Date.now() - pulseFrom), rt.v2Doing, stale))
|
|
1657
1749
|
}, PULSE_MS)
|
|
1658
1750
|
if (pulseTimer.unref) pulseTimer.unref()
|
|
1659
1751
|
}
|
|
@@ -1872,6 +1964,7 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1872
1964
|
const projected = rt?.v2TerminalProjection
|
|
1873
1965
|
const resultText = projected?.payload?.resultText
|
|
1874
1966
|
const quotaFailure = supportedV2QuotaFailure(payload, rt?.v2RunnerResult)
|
|
1967
|
+
const closedFailure = supportedV2ClosedFailure(payload, rt?.v2RunnerResult)
|
|
1875
1968
|
if (!/^[0-9a-f]{32}$/.test(receiptId || '') ||
|
|
1876
1969
|
!Number.isSafeInteger(turnToken) || turnToken <= 0 ||
|
|
1877
1970
|
typeof effectId !== 'string' || !effectId ||
|
|
@@ -1903,10 +1996,9 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1903
1996
|
(!isDeepStrictEqual(payload.batchReceiptIds, cohort.receiptIds) ||
|
|
1904
1997
|
payload.replyTargetReceiptId !== receiptId)) ||
|
|
1905
1998
|
!isDeepStrictEqual(payload, projected.payload) ||
|
|
1906
|
-
//
|
|
1907
|
-
//
|
|
1908
|
-
|
|
1909
|
-
payload.source !== 'result' || payload.aborted || (payload.isError && !quotaFailure) ||
|
|
1999
|
+
// A deterministic failure is delivered once, after the same durable
|
|
2000
|
+
// close checks as success. Retry and unknown outcomes stay sealed.
|
|
2001
|
+
(payload.source !== 'result' && !closedFailure) || payload.aborted || (payload.isError && !quotaFailure && !closedFailure) ||
|
|
1910
2002
|
payload.sessionMissing || (payload.hasQuota && !quotaFailure) || payload.transientNetwork ||
|
|
1911
2003
|
payload.freshRetried || payload.resumedSessionCurrent ||
|
|
1912
2004
|
(replyText !== undefined && replyText !== terminalReplyText(projected.payload)) ||
|
|
@@ -1920,6 +2012,9 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1920
2012
|
value.generation === generation && rt.v2RunnerGeneration === generation
|
|
1921
2013
|
try {
|
|
1922
2014
|
const preopened = rt.v2CardBinding
|
|
2015
|
+
if (closedFailure) authority.assertClosedFailureReady({ receiptId, chatId,
|
|
2016
|
+
turnToken, generation, verifyGeneration,
|
|
2017
|
+
assertWriterLock: this.durableV2AssertWriterLock })
|
|
1923
2018
|
if (preopened && (preopened.receiptId !== receiptId ||
|
|
1924
2019
|
preopened.turnToken !== turnToken ||
|
|
1925
2020
|
preopened.generation !== generation)) {
|
|
@@ -1927,7 +2022,8 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1927
2022
|
}
|
|
1928
2023
|
const path = preopened?.path || prepareCardTerminalPath({ runtime: authority,
|
|
1929
2024
|
payloadHandles: handles, platform: job.platform,
|
|
1930
|
-
replyCtx: job.msg.replyCtx,
|
|
2025
|
+
replyCtx: job.msg.replyCtx, cardAnchor: rt.v2CardAnchor, preferPlain: closedFailure,
|
|
2026
|
+
receiptId, chatId, turnToken, generation,
|
|
1931
2027
|
verifyGeneration, applyLocalEffect: this.durableV2ApplyLocalEffect,
|
|
1932
2028
|
assertWriterLock: this.durableV2AssertWriterLock,
|
|
1933
2029
|
redactText: text => redact(quotaFailure
|
|
@@ -1942,6 +2038,8 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1942
2038
|
for (const text of updates) path.update(text)
|
|
1943
2039
|
const footer = T.cardFooter({ ms: Math.max(0, Date.now() - rt.v2StartedAt),
|
|
1944
2040
|
tokens: rt.v2UsageDecision?.tokens || null })
|
|
2041
|
+
const footerDetail = job.platform.features?.cardQuota === true
|
|
2042
|
+
? cardModelDetail(rt.v2CardModelInfo, rt.v2RunnerResult) : ''
|
|
1945
2043
|
// Milestones from tool calls first, then the model's own `>> ` lines;
|
|
1946
2044
|
// the card shows them in its "process" fold (display only).
|
|
1947
2045
|
rt.v2CardQuotaFailure = !!quotaFailure
|
|
@@ -1950,7 +2048,8 @@ async completeDurableV2CardTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
1950
2048
|
...progress.milestonesOf(payload.bufferText)]
|
|
1951
2049
|
.filter((m, i, a) => a.indexOf(m) === i).map(redactMilestone)
|
|
1952
2050
|
.filter(m => typeof m === 'string' && m.trim())
|
|
1953
|
-
const completed = await path.complete({ payload, replyText, paused,
|
|
2051
|
+
const completed = await path.complete({ payload, replyText, paused,
|
|
2052
|
+
footer, footerDetail, milestones })
|
|
1954
2053
|
const after = authority.snapshot()
|
|
1955
2054
|
const queue = after.state?.lanes?.queue?.[chatId]?.queue
|
|
1956
2055
|
let successor = null
|
|
@@ -2064,7 +2163,7 @@ classifyDurableV2UnresolvedTurn({ chatId, receiptId, turnToken, effectId,
|
|
|
2064
2163
|
}
|
|
2065
2164
|
rt.v2TerminalStarted = true
|
|
2066
2165
|
authority.registerTerminalCallback({ receiptId, generation,
|
|
2067
|
-
registeredAt: Date.now() })
|
|
2166
|
+
registeredAt: Date.now(), reuse: true })
|
|
2068
2167
|
return authority.commitUnresolvedClassification({ receiptId, generation,
|
|
2069
2168
|
turnToken, status, payload,
|
|
2070
2169
|
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') {
|