openclaw-channel-dmwork 0.6.1 → 0.6.2-dev.84da5e15
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/index.js +21 -4
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/src/api-fetch.d.ts +0 -13
- package/dist/src/api-fetch.js +0 -12
- package/dist/src/api-fetch.js.map +1 -1
- package/dist/src/channel.js +13 -1
- package/dist/src/channel.js.map +1 -1
- package/dist/src/group-md.d.ts +2 -1
- package/dist/src/group-md.js +6 -17
- package/dist/src/group-md.js.map +1 -1
- package/dist/src/inbound.d.ts +4 -0
- package/dist/src/inbound.js +153 -154
- package/dist/src/inbound.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/package.json +1 -1
- package/skills/dmwork-bot-api/SKILL.md +1 -1
package/dist/src/inbound.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sendMessage,
|
|
1
|
+
import { sendMessage, sendReadReceipt, sendTyping, getChannelMessages, getGroupMembers, getGroupMd, sendMediaMessage, inferContentType, ensureTextCharset, parseImageDimensionsFromFile, getUploadCredentials, uploadFileToCOS, fetchUserInfo } from "./api-fetch.js";
|
|
2
2
|
import { ChannelType, MessageType } from "./types.js";
|
|
3
3
|
import { getDmworkRuntime } from "./runtime.js";
|
|
4
4
|
import { DEFAULT_HISTORY_PROMPT_TEMPLATE } from "./config-schema.js";
|
|
@@ -8,6 +8,9 @@ import { createWriteStream } from "node:fs";
|
|
|
8
8
|
import { mkdir, unlink, readdir, stat } from "node:fs/promises";
|
|
9
9
|
import { join, basename } from "node:path";
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
// Pending inbound context for before_prompt_build hook injection.
|
|
12
|
+
// handleInboundMessage writes here; the hook reads and clears per sessionKey.
|
|
13
|
+
export const pendingInboundContext = new Map();
|
|
11
14
|
// Defensive imports — these may not exist in older OpenClaw versions
|
|
12
15
|
// History context managed manually for cross-SDK compatibility
|
|
13
16
|
let clearHistoryEntriesIfEnabled;
|
|
@@ -1203,11 +1206,13 @@ export async function handleInboundMessage(params) {
|
|
|
1203
1206
|
storePath,
|
|
1204
1207
|
sessionKey: route.sessionKey,
|
|
1205
1208
|
});
|
|
1206
|
-
//
|
|
1209
|
+
// memberListPrefix and historyPrefix are injected via before_prompt_build hook
|
|
1210
|
+
// (not persisted to session history). Only quotePrefix stays in Body.
|
|
1207
1211
|
const memberListPrefix = isGroup ? buildMemberListPrefix(uidToNameMap) : "";
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1212
|
+
if (historyPrefix || memberListPrefix) {
|
|
1213
|
+
pendingInboundContext.set(route.sessionKey, { historyPrefix, memberListPrefix });
|
|
1214
|
+
}
|
|
1215
|
+
const finalBody = quotePrefix ? (quotePrefix + rawBody) : rawBody;
|
|
1211
1216
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
1212
1217
|
channel: "DMWork",
|
|
1213
1218
|
from: fromLabel,
|
|
@@ -1216,10 +1221,8 @@ export async function handleInboundMessage(params) {
|
|
|
1216
1221
|
envelope: envelopeOptions,
|
|
1217
1222
|
body: finalBody,
|
|
1218
1223
|
});
|
|
1219
|
-
//
|
|
1220
|
-
|
|
1221
|
-
? groupMdCache.get(extractParentGroupNo(message.channel_id))?.content
|
|
1222
|
-
: undefined;
|
|
1224
|
+
// GROUP.md injection is handled exclusively by the before_prompt_build hook
|
|
1225
|
+
// (see index.ts → getGroupMdForPrompt) — no longer set here to avoid duplication.
|
|
1223
1226
|
// Resolve sender display name — async fallback for DM users not in cache
|
|
1224
1227
|
let senderName = resolveSenderName(message.from_uid, uidToNameMap);
|
|
1225
1228
|
if (!senderName && !isGroup) {
|
|
@@ -1245,9 +1248,10 @@ export async function handleInboundMessage(params) {
|
|
|
1245
1248
|
}
|
|
1246
1249
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
1247
1250
|
Body: body,
|
|
1248
|
-
BodyForAgent: body,
|
|
1251
|
+
BodyForAgent: body,
|
|
1249
1252
|
RawBody: rawBody,
|
|
1250
1253
|
CommandBody: rawBody,
|
|
1254
|
+
CommandAuthorized: true,
|
|
1251
1255
|
MediaUrl: isFileMessage ? undefined : inboundMediaUrl,
|
|
1252
1256
|
MediaUrls: (() => {
|
|
1253
1257
|
// Only pass current message's local media path (no remote history URLs)
|
|
@@ -1268,7 +1272,7 @@ export async function handleInboundMessage(params) {
|
|
|
1268
1272
|
MessageSid: String(message.message_id),
|
|
1269
1273
|
Timestamp: message.timestamp ? message.timestamp * 1000 : undefined,
|
|
1270
1274
|
GroupSubject: isGroup ? message.channel_id : undefined,
|
|
1271
|
-
GroupSystemPrompt:
|
|
1275
|
+
GroupSystemPrompt: undefined,
|
|
1272
1276
|
Provider: "dmwork",
|
|
1273
1277
|
Surface: "dmwork",
|
|
1274
1278
|
OriginatingChannel: "dmwork",
|
|
@@ -1300,21 +1304,126 @@ export async function handleInboundMessage(params) {
|
|
|
1300
1304
|
const typingInterval = setInterval(() => {
|
|
1301
1305
|
sendTyping({ apiUrl, botToken, channelId: replyChannelId, channelType: replyChannelType }).catch(() => { });
|
|
1302
1306
|
}, 5000);
|
|
1303
|
-
//
|
|
1304
|
-
//
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
+
// Buffer text across streaming deliver calls; only send once after dispatcher finishes.
|
|
1308
|
+
// Media is sent immediately (no edit problem); text is buffered (each call overwrites).
|
|
1309
|
+
const deliverBuffer = {
|
|
1310
|
+
lastText: null,
|
|
1311
|
+
textSent: false,
|
|
1312
|
+
};
|
|
1313
|
+
const sentMediaUrls = new Set();
|
|
1314
|
+
// --- Shared helper: resolve mentions and send text ---
|
|
1315
|
+
const resolveAndSendText = async (content) => {
|
|
1316
|
+
let replyMentionUids = [];
|
|
1317
|
+
let replyMentionEntities = [];
|
|
1318
|
+
let finalContent = content;
|
|
1319
|
+
if (isGroup) {
|
|
1320
|
+
const structuredMentions = parseStructuredMentions(content);
|
|
1321
|
+
if (structuredMentions.length > 0) {
|
|
1322
|
+
// v2 path: LLM used @[uid:name] format
|
|
1323
|
+
const validUids = new Set(uidToNameMap.keys());
|
|
1324
|
+
const converted = convertStructuredMentions(content, structuredMentions, validUids);
|
|
1325
|
+
finalContent = converted.content;
|
|
1326
|
+
replyMentionEntities = [...converted.entities];
|
|
1327
|
+
// Mixed scenario: check for remaining @name in converted content
|
|
1328
|
+
const remaining = buildEntitiesFromFallback(finalContent, memberMap);
|
|
1329
|
+
const existingOffsets = new Set(replyMentionEntities.map((e) => e.offset));
|
|
1330
|
+
for (const rm of remaining.entities) {
|
|
1331
|
+
if (!existingOffsets.has(rm.offset)) {
|
|
1332
|
+
replyMentionEntities.push(rm);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
log?.debug?.(`dmwork: [REPLY] structured mentions: ${structuredMentions.length}, fallback: ${remaining.entities.length}`);
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
// v1 fallback path: LLM used @name format
|
|
1339
|
+
const contentMentions = extractMentionMatches(content);
|
|
1340
|
+
const unresolvedNames = [];
|
|
1341
|
+
const resolveMention = (name) => {
|
|
1342
|
+
const uid = findUidByName(name, memberMap);
|
|
1343
|
+
let newContent = finalContent;
|
|
1344
|
+
if (uid) {
|
|
1345
|
+
return { uid, newContent };
|
|
1346
|
+
}
|
|
1347
|
+
else if (/^[a-f0-9]{32}$/i.test(name)) {
|
|
1348
|
+
const displayName = uidToNameMap.get(name);
|
|
1349
|
+
if (displayName) {
|
|
1350
|
+
newContent = newContent.replace(`@${name}`, `@${displayName}`);
|
|
1351
|
+
return { uid: name, newContent };
|
|
1352
|
+
}
|
|
1353
|
+
return { uid: name, newContent };
|
|
1354
|
+
}
|
|
1355
|
+
else if (/^[a-zA-Z0-9_]+$/.test(name)) {
|
|
1356
|
+
const displayName = uidToNameMap.get(name);
|
|
1357
|
+
if (displayName) {
|
|
1358
|
+
newContent = newContent.replace(`@${name}`, `@${displayName}`);
|
|
1359
|
+
return { uid: name, newContent };
|
|
1360
|
+
}
|
|
1361
|
+
return { uid: name, newContent };
|
|
1362
|
+
}
|
|
1363
|
+
return { uid: null, newContent };
|
|
1364
|
+
};
|
|
1365
|
+
const resolvedUids = [];
|
|
1366
|
+
for (const mention of contentMentions) {
|
|
1367
|
+
const name = mention.slice(1);
|
|
1368
|
+
const result = resolveMention(name);
|
|
1369
|
+
finalContent = result.newContent;
|
|
1370
|
+
resolvedUids.push(result.uid);
|
|
1371
|
+
if (!result.uid) {
|
|
1372
|
+
unresolvedNames.push({ name, index: resolvedUids.length - 1 });
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (unresolvedNames.length > 0) {
|
|
1376
|
+
log?.info?.(`dmwork: [REPLY] ${unresolvedNames.length} unresolved names, force refreshing cache...`);
|
|
1377
|
+
const refreshed = await refreshGroupMemberCache({ sessionId: memberCacheGroupNo, memberMap, uidToNameMap, groupCacheTimestamps, apiUrl: account.config.apiUrl, botToken: account.config.botToken ?? "", forceRefresh: true, log });
|
|
1378
|
+
if (refreshed) {
|
|
1379
|
+
for (const { name, index } of unresolvedNames) {
|
|
1380
|
+
const uid = findUidByName(name, memberMap);
|
|
1381
|
+
if (uid) {
|
|
1382
|
+
resolvedUids[index] = uid;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
replyMentionUids = resolvedUids.filter((uid) => uid !== null);
|
|
1388
|
+
const fallbackResult = buildEntitiesFromFallback(finalContent, memberMap);
|
|
1389
|
+
replyMentionEntities = fallbackResult.entities;
|
|
1390
|
+
}
|
|
1391
|
+
// Sort entities by offset and rebuild uids from sorted entities
|
|
1392
|
+
if (replyMentionEntities.length > 0) {
|
|
1393
|
+
replyMentionEntities.sort((a, b) => a.offset - b.offset);
|
|
1394
|
+
replyMentionUids = replyMentionEntities.map((e) => e.uid);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
// Detect @all/@所有人 in final content
|
|
1398
|
+
const hasAtAll = /(?:^|(?<=\s))@(?:all|所有人)(?=\s|[^\w]|$)/i.test(finalContent);
|
|
1399
|
+
await sendMessage({
|
|
1400
|
+
apiUrl: account.config.apiUrl,
|
|
1401
|
+
botToken: account.config.botToken ?? "",
|
|
1402
|
+
channelId: replyChannelId,
|
|
1403
|
+
channelType: replyChannelType,
|
|
1404
|
+
content: finalContent,
|
|
1405
|
+
...(replyMentionUids.length > 0 ? { mentionUids: replyMentionUids } : {}),
|
|
1406
|
+
...(replyMentionEntities.length > 0 ? { mentionEntities: replyMentionEntities } : {}),
|
|
1407
|
+
mentionAll: hasAtAll || undefined,
|
|
1408
|
+
});
|
|
1409
|
+
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
1410
|
+
};
|
|
1307
1411
|
try {
|
|
1308
1412
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1309
1413
|
ctx: ctxPayload,
|
|
1310
1414
|
cfg: config,
|
|
1311
1415
|
replyOptions: {},
|
|
1312
1416
|
dispatcherOptions: {
|
|
1313
|
-
deliver: async (payload) => {
|
|
1314
|
-
//
|
|
1417
|
+
deliver: async (payload, info) => {
|
|
1418
|
+
// Skip reasoning blocks
|
|
1419
|
+
if (payload.isReasoning)
|
|
1420
|
+
return;
|
|
1421
|
+
const kind = info?.kind ?? "final";
|
|
1422
|
+
// --- Media: send immediately (no edit/forward issue) with dedup ---
|
|
1315
1423
|
const outboundMediaUrls = resolveOutboundMediaUrls(payload);
|
|
1316
|
-
// Upload and send each media file
|
|
1317
1424
|
for (const mediaUrl of outboundMediaUrls) {
|
|
1425
|
+
if (sentMediaUrls.has(mediaUrl))
|
|
1426
|
+
continue;
|
|
1318
1427
|
try {
|
|
1319
1428
|
await uploadAndSendMedia({
|
|
1320
1429
|
mediaUrl,
|
|
@@ -1324,12 +1433,13 @@ export async function handleInboundMessage(params) {
|
|
|
1324
1433
|
channelType: replyChannelType,
|
|
1325
1434
|
log,
|
|
1326
1435
|
});
|
|
1436
|
+
sentMediaUrls.add(mediaUrl);
|
|
1327
1437
|
}
|
|
1328
1438
|
catch (err) {
|
|
1329
1439
|
log?.error?.(`dmwork: media send failed for ${mediaUrl}: ${String(err)}`);
|
|
1330
1440
|
}
|
|
1331
1441
|
}
|
|
1332
|
-
//
|
|
1442
|
+
// --- Text handling based on kind ---
|
|
1333
1443
|
const content = payload.text?.trim() ?? "";
|
|
1334
1444
|
if (!content && outboundMediaUrls.length > 0) {
|
|
1335
1445
|
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
@@ -1337,146 +1447,22 @@ export async function handleInboundMessage(params) {
|
|
|
1337
1447
|
}
|
|
1338
1448
|
if (!content)
|
|
1339
1449
|
return;
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
if (isGroup) {
|
|
1346
|
-
const structuredMentions = parseStructuredMentions(content);
|
|
1347
|
-
if (structuredMentions.length > 0) {
|
|
1348
|
-
// v2 path: LLM used @[uid:name] format
|
|
1349
|
-
const validUids = new Set(uidToNameMap.keys());
|
|
1350
|
-
const converted = convertStructuredMentions(content, structuredMentions, validUids);
|
|
1351
|
-
finalContent = converted.content;
|
|
1352
|
-
replyMentionEntities = [...converted.entities];
|
|
1353
|
-
// Mixed scenario: check for remaining @name in converted content
|
|
1354
|
-
const remaining = buildEntitiesFromFallback(finalContent, memberMap);
|
|
1355
|
-
const existingOffsets = new Set(replyMentionEntities.map((e) => e.offset));
|
|
1356
|
-
for (const rm of remaining.entities) {
|
|
1357
|
-
if (!existingOffsets.has(rm.offset)) {
|
|
1358
|
-
replyMentionEntities.push(rm);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
log?.debug?.(`dmwork: [REPLY] structured mentions: ${structuredMentions.length}, fallback: ${remaining.entities.length}`);
|
|
1362
|
-
}
|
|
1363
|
-
else {
|
|
1364
|
-
// v1 fallback path: LLM used @name format
|
|
1365
|
-
// Keep existing resolveMention logic for hex uid / uid-format handling
|
|
1366
|
-
const contentMentions = extractMentionMatches(content);
|
|
1367
|
-
let unresolvedNames = [];
|
|
1368
|
-
const resolveMention = (name) => {
|
|
1369
|
-
let uid = findUidByName(name, memberMap);
|
|
1370
|
-
let newContent = finalContent;
|
|
1371
|
-
if (uid) {
|
|
1372
|
-
return { uid, newContent };
|
|
1373
|
-
}
|
|
1374
|
-
else if (/^[a-f0-9]{32}$/i.test(name)) {
|
|
1375
|
-
const displayName = uidToNameMap.get(name);
|
|
1376
|
-
if (displayName) {
|
|
1377
|
-
newContent = newContent.replace(`@${name}`, `@${displayName}`);
|
|
1378
|
-
return { uid: name, newContent };
|
|
1379
|
-
}
|
|
1380
|
-
return { uid: name, newContent };
|
|
1381
|
-
}
|
|
1382
|
-
else if (/^[a-zA-Z0-9_]+$/.test(name)) {
|
|
1383
|
-
const displayName = uidToNameMap.get(name);
|
|
1384
|
-
if (displayName) {
|
|
1385
|
-
newContent = newContent.replace(`@${name}`, `@${displayName}`);
|
|
1386
|
-
return { uid: name, newContent };
|
|
1387
|
-
}
|
|
1388
|
-
return { uid: name, newContent };
|
|
1389
|
-
}
|
|
1390
|
-
return { uid: null, newContent };
|
|
1391
|
-
};
|
|
1392
|
-
const resolvedUids = [];
|
|
1393
|
-
for (const mention of contentMentions) {
|
|
1394
|
-
const name = mention.slice(1);
|
|
1395
|
-
const result = resolveMention(name);
|
|
1396
|
-
finalContent = result.newContent;
|
|
1397
|
-
resolvedUids.push(result.uid);
|
|
1398
|
-
if (!result.uid) {
|
|
1399
|
-
unresolvedNames.push({ name, index: resolvedUids.length - 1 });
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
if (unresolvedNames.length > 0) {
|
|
1403
|
-
log?.info?.(`dmwork: [REPLY] ${unresolvedNames.length} unresolved names, force refreshing cache...`);
|
|
1404
|
-
const refreshed = await refreshGroupMemberCache({ sessionId: memberCacheGroupNo, memberMap, uidToNameMap, groupCacheTimestamps, apiUrl: account.config.apiUrl, botToken: account.config.botToken ?? "", forceRefresh: true, log });
|
|
1405
|
-
if (refreshed) {
|
|
1406
|
-
for (const { name, index } of unresolvedNames) {
|
|
1407
|
-
const uid = findUidByName(name, memberMap);
|
|
1408
|
-
if (uid) {
|
|
1409
|
-
resolvedUids[index] = uid;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
replyMentionUids = resolvedUids.filter((uid) => uid !== null);
|
|
1415
|
-
// Build entities from fallback for the final content
|
|
1416
|
-
const fallbackResult = buildEntitiesFromFallback(finalContent, memberMap);
|
|
1417
|
-
replyMentionEntities = fallbackResult.entities;
|
|
1418
|
-
}
|
|
1419
|
-
// Sort entities by offset and rebuild uids from sorted entities
|
|
1420
|
-
if (replyMentionEntities.length > 0) {
|
|
1421
|
-
replyMentionEntities.sort((a, b) => a.offset - b.offset);
|
|
1422
|
-
replyMentionUids = replyMentionEntities.map((e) => e.uid);
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
// Detect @all/@所有人 in final content
|
|
1426
|
-
const hasAtAll = /(?:^|(?<=\s))@(?:all|所有人)(?=\s|[^\w]|$)/i.test(finalContent);
|
|
1427
|
-
// Draft-edit dedup: subsequent delivers edit the first message.
|
|
1428
|
-
// Note: editMessage API only updates content_edit text; mention metadata
|
|
1429
|
-
// (mentionUids/mentionEntities/mentionAll) cannot be updated via edit.
|
|
1430
|
-
// This is acceptable because the first sendMessage already carries the
|
|
1431
|
-
// correct mention info, and subsequent edits only update the text body.
|
|
1432
|
-
if (draftMessage) {
|
|
1433
|
-
try {
|
|
1434
|
-
const contentEdit = JSON.stringify({ type: 1, content: finalContent });
|
|
1435
|
-
log?.debug?.(`dmwork: [draft-edit] editMessage attempt id=${draftMessage.messageId} seq=${draftMessage.messageSeq}`);
|
|
1436
|
-
await editMessage({
|
|
1437
|
-
apiUrl: account.config.apiUrl,
|
|
1438
|
-
botToken: account.config.botToken ?? "",
|
|
1439
|
-
messageId: draftMessage.messageId,
|
|
1440
|
-
messageSeq: draftMessage.messageSeq,
|
|
1441
|
-
channelId: replyChannelId,
|
|
1442
|
-
channelType: replyChannelType,
|
|
1443
|
-
contentEdit,
|
|
1444
|
-
});
|
|
1445
|
-
log?.debug?.(`dmwork: [draft-edit] editMessage success`);
|
|
1446
|
-
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|
|
1447
|
-
return;
|
|
1448
|
-
}
|
|
1449
|
-
catch (editErr) {
|
|
1450
|
-
log?.warn?.(`dmwork: [draft-edit] editMessage failed: ${String(editErr)}, falling back to sendMessage`);
|
|
1451
|
-
draftMessage = null;
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
const sendResult = await sendMessage({
|
|
1455
|
-
apiUrl: account.config.apiUrl,
|
|
1456
|
-
botToken: account.config.botToken ?? "",
|
|
1457
|
-
channelId: replyChannelId,
|
|
1458
|
-
channelType: replyChannelType,
|
|
1459
|
-
content: finalContent,
|
|
1460
|
-
...(replyMentionUids.length > 0 ? { mentionUids: replyMentionUids } : {}),
|
|
1461
|
-
...(replyMentionEntities.length > 0 ? { mentionEntities: replyMentionEntities } : {}),
|
|
1462
|
-
mentionAll: hasAtAll || undefined,
|
|
1463
|
-
});
|
|
1464
|
-
// Save draft for subsequent edit-dedup
|
|
1465
|
-
if (sendResult?.message_id != null && sendResult?.message_seq != null) {
|
|
1466
|
-
draftMessage = {
|
|
1467
|
-
messageId: String(sendResult.message_id),
|
|
1468
|
-
messageSeq: sendResult.message_seq,
|
|
1469
|
-
};
|
|
1470
|
-
log?.debug?.(`dmwork: [draft-edit] sendMessage OK, draft set: id=${draftMessage.messageId} seq=${draftMessage.messageSeq}`);
|
|
1471
|
-
}
|
|
1472
|
-
else {
|
|
1473
|
-
log?.debug?.(`dmwork: [draft-edit] sendMessage returned no id/seq, draft-edit disabled`);
|
|
1450
|
+
if (kind === "tool") {
|
|
1451
|
+
// Verbose tool call output: send immediately
|
|
1452
|
+
await resolveAndSendText(content);
|
|
1453
|
+
log?.info?.(`dmwork: [deliver] tool text sent (${content.length} chars)`);
|
|
1454
|
+
return;
|
|
1474
1455
|
}
|
|
1475
|
-
|
|
1456
|
+
// kind === "block" / "final" / anything else: buffer, send only once after dispatcher finishes
|
|
1457
|
+
deliverBuffer.lastText = content;
|
|
1458
|
+
log?.debug?.(`dmwork: [deliver-buffer] ${kind} text buffered (${content.length} chars)`);
|
|
1476
1459
|
},
|
|
1477
1460
|
onError: async (err, info) => {
|
|
1478
1461
|
clearInterval(typingInterval);
|
|
1479
1462
|
log?.error?.(`dmwork ${info.kind} reply failed: ${String(err)}`);
|
|
1463
|
+
// Prevent finally block from sending stale buffered text after error
|
|
1464
|
+
deliverBuffer.lastText = null;
|
|
1465
|
+
deliverBuffer.textSent = true;
|
|
1480
1466
|
try {
|
|
1481
1467
|
await sendMessage({
|
|
1482
1468
|
apiUrl,
|
|
@@ -1494,7 +1480,20 @@ export async function handleInboundMessage(params) {
|
|
|
1494
1480
|
});
|
|
1495
1481
|
}
|
|
1496
1482
|
finally {
|
|
1483
|
+
// --- Final send: deliver buffered text if only blocks arrived (no final/tool) ---
|
|
1484
|
+
if (deliverBuffer.lastText && !deliverBuffer.textSent) {
|
|
1485
|
+
deliverBuffer.textSent = true;
|
|
1486
|
+
try {
|
|
1487
|
+
await resolveAndSendText(deliverBuffer.lastText);
|
|
1488
|
+
log?.info?.(`dmwork: [deliver-buffer] fallback text sent (${deliverBuffer.lastText.length} chars)`);
|
|
1489
|
+
}
|
|
1490
|
+
catch (finalSendErr) {
|
|
1491
|
+
log?.error?.(`dmwork: [deliver-buffer] final text send failed: ${String(finalSendErr)}`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1497
1494
|
clearInterval(typingInterval);
|
|
1495
|
+
// Safety net: clean up pending inbound context in case the hook didn't fire
|
|
1496
|
+
pendingInboundContext.delete(route.sessionKey);
|
|
1498
1497
|
}
|
|
1499
1498
|
}
|
|
1500
1499
|
//# sourceMappingURL=inbound.js.map
|