mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-18 04:50:44 +08:00
fix(delivery): surface failed semantic finals
This commit is contained in:
parent
d7f4cf03ab
commit
fde088e381
3 changed files with 139 additions and 3 deletions
File diff suppressed because one or more lines are too long
|
|
@ -558,10 +558,18 @@ export class TextHarnessBridge {
|
||||||
textDeliveryError = error;
|
textDeliveryError = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (textReceipt?.deliveryOutcome === 'failed') {
|
||||||
|
const reason = textReceipt.reason ?? 'text-delivery-failed';
|
||||||
|
textDeliveryError = new Error(`Final text delivery failed (${reason})`);
|
||||||
|
textDeliveryError.code = reason;
|
||||||
|
}
|
||||||
// A failed final text must not discard an already registered result file.
|
// A failed final text must not discard an already registered result file.
|
||||||
// Settle the independent attachment path before surfacing the text error.
|
// Settle the independent attachment path before surfacing the text error.
|
||||||
const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
|
const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
|
||||||
if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
|
if (textDeliveryError && !delivery.userVisible) {
|
||||||
|
textDeliveryError.deliveryReceipt = delivery.receipt;
|
||||||
|
throw textDeliveryError;
|
||||||
|
}
|
||||||
if (delivery.userVisible) {
|
if (delivery.userVisible) {
|
||||||
this.#status.messagesReplied += 1;
|
this.#status.messagesReplied += 1;
|
||||||
this.#status.lastReplyAt = new Date().toISOString();
|
this.#status.lastReplyAt = new Date().toISOString();
|
||||||
|
|
@ -626,7 +634,9 @@ export class TextHarnessBridge {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a message:`, error);
|
this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a message:`, error);
|
||||||
if (await presentStreamFailure('消息处理失败,请稍后重试。')) return;
|
if (await presentStreamFailure('消息处理失败,请稍后重试。')) {
|
||||||
|
return error.deliveryReceipt;
|
||||||
|
}
|
||||||
stream?.cancel?.();
|
stream?.cancel?.();
|
||||||
try {
|
try {
|
||||||
await this.#bot.sendText(target, '消息处理失败,请稍后重试。');
|
await this.#bot.sendText(target, '消息处理失败,请稍后重试。');
|
||||||
|
|
@ -636,6 +646,7 @@ export class TextHarnessBridge {
|
||||||
sendError,
|
sendError,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return error.deliveryReceipt;
|
||||||
} finally {
|
} finally {
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
this.#cancelPendingInteraction(conversationKey),
|
this.#cancelPendingInteraction(conversationKey),
|
||||||
|
|
|
||||||
|
|
@ -510,6 +510,131 @@ test('Telegram uncertain final delivery records unknown and never sends a fallba
|
||||||
assert.deepEqual(groupResult.providerMessageIds, ['1202']);
|
assert.deepEqual(groupResult.providerMessageIds, ['1202']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shared bridge surfaces a definite final delivery failure without rerunning Prompt or artifacts', async (t) => {
|
||||||
|
for (const safeReplyFails of [false, true]) {
|
||||||
|
await t.test(safeReplyFails ? 'safe reply also fails' : 'safe reply succeeds', async () => {
|
||||||
|
const safeReplies = [];
|
||||||
|
let prompts = 0;
|
||||||
|
const stream = {
|
||||||
|
providerMessageIds: ['failed-placeholder'],
|
||||||
|
presentation: 'telegram-regular',
|
||||||
|
async update() {},
|
||||||
|
async finish() {
|
||||||
|
return {
|
||||||
|
presentation: 'telegram-rich-final',
|
||||||
|
providerMessageIds: ['failed-placeholder'],
|
||||||
|
deliveryOutcome: 'failed',
|
||||||
|
reason: 'telegram-403',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async fail() {
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
cancel() {},
|
||||||
|
};
|
||||||
|
const bridge = new TelegramHarnessBridge({
|
||||||
|
bot: {
|
||||||
|
sendTyping: async () => {},
|
||||||
|
openDeliveryStream: async () => stream,
|
||||||
|
sendText: async (_target, text) => {
|
||||||
|
safeReplies.push(text);
|
||||||
|
if (safeReplyFails) throw rejected(403);
|
||||||
|
return { message_id: 1261 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: memoryState(),
|
||||||
|
logger: { warn() {}, error() {} },
|
||||||
|
harness: {
|
||||||
|
createSession: async () => 'session-rich-artifact',
|
||||||
|
ask: async () => {
|
||||||
|
prompts += 1;
|
||||||
|
return '## final';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const receipt = await bridge.accept({
|
||||||
|
messageId: `definite-failure-${safeReplyFails}`,
|
||||||
|
senderId: 'rich-user',
|
||||||
|
kind: 'direct',
|
||||||
|
conversationId: `definite-failure-${safeReplyFails}`,
|
||||||
|
content: 'return a file',
|
||||||
|
addressed: true,
|
||||||
|
replyTarget: { chatId: 42, chatType: 'private', replyToMessageId: 44 },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(prompts, 1);
|
||||||
|
assert.deepEqual(safeReplies, ['消息处理失败,请稍后重试。']);
|
||||||
|
assert.equal(receipt.deliveryOutcome, 'failed');
|
||||||
|
assert.equal(receipt.reason, 'telegram-403');
|
||||||
|
assert.deepEqual(receipt.artifacts, []);
|
||||||
|
assert.match(bridge.status.lastError, /telegram-403/);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shared bridge settles each artifact once before containing a definite text failure', async (t) => {
|
||||||
|
const artifact = await committedArtifact(t);
|
||||||
|
const sentFiles = [];
|
||||||
|
let prompts = 0;
|
||||||
|
let safeReplies = 0;
|
||||||
|
const bridge = new TelegramHarnessBridge({
|
||||||
|
bot: {
|
||||||
|
sendTyping: async () => {},
|
||||||
|
openDeliveryStream: async () => ({
|
||||||
|
providerMessageIds: ['failed-placeholder'],
|
||||||
|
presentation: 'telegram-regular',
|
||||||
|
async update() {},
|
||||||
|
async finish() {
|
||||||
|
return {
|
||||||
|
presentation: 'telegram-rich-final',
|
||||||
|
providerMessageIds: ['failed-placeholder'],
|
||||||
|
deliveryOutcome: 'failed',
|
||||||
|
reason: 'telegram-403',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cancel() {},
|
||||||
|
}),
|
||||||
|
sendText: async () => {
|
||||||
|
safeReplies += 1;
|
||||||
|
return { message_id: 1261 };
|
||||||
|
},
|
||||||
|
sendFile: async (_target, file) => {
|
||||||
|
sentFiles.push(file.fileName);
|
||||||
|
return { message_id: 1262 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: memoryState(),
|
||||||
|
logger: { warn() {}, error() {} },
|
||||||
|
harness: {
|
||||||
|
createSession: async () => 'session-rich-artifact',
|
||||||
|
ask: async (_sessionId, _text, options) => {
|
||||||
|
prompts += 1;
|
||||||
|
await options.onArtifact(artifact);
|
||||||
|
return '## final with file';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const receipt = await bridge.accept({
|
||||||
|
messageId: 'definite-failure-with-file',
|
||||||
|
senderId: 'rich-user',
|
||||||
|
kind: 'direct',
|
||||||
|
conversationId: 'definite-failure-with-file',
|
||||||
|
content: 'return a file',
|
||||||
|
addressed: true,
|
||||||
|
replyTarget: { chatId: 42, chatType: 'private', replyToMessageId: 44 },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(prompts, 1);
|
||||||
|
assert.equal(safeReplies, 0);
|
||||||
|
assert.deepEqual(sentFiles, ['result.txt']);
|
||||||
|
assert.equal(receipt.deliveryOutcome, 'failed');
|
||||||
|
assert.deepEqual(receipt.artifacts, [{
|
||||||
|
artifactId: 'telegram-rich-artifact', outcome: 'sent',
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
test('Telegram distinguishes pre-dispatch abort from an uncertain in-flight abort', async () => {
|
test('Telegram distinguishes pre-dispatch abort from an uncertain in-flight abort', async () => {
|
||||||
const before = new AbortController();
|
const before = new AbortController();
|
||||||
const beforeReason = new DOMException('stopped before dispatch', 'AbortError');
|
const beforeReason = new DOMException('stopped before dispatch', 'AbortError');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue