openclaw-lark-multi-agent 1.8.9 → 1.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +20 -0
- package/dist/feishu-bot.d.ts +8 -0
- package/dist/feishu-bot.js +269 -40
- package/dist/message-store.d.ts +13 -0
- package/dist/message-store.js +61 -1
- package/dist/openclaw-client.d.ts +42 -6
- package/dist/openclaw-client.js +353 -28
- package/dist/session-status.d.ts +2 -0
- package/dist/session-status.js +2 -0
- package/docs/releases/1.8.10.md +31 -0
- package/docs/releases/1.8.11.md +47 -0
- package/package.json +5 -3
- package/scripts/update-lma-core.sh +3 -2
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ Usage:
|
|
|
24
24
|
${CLI_NAME} install-windows-service [--state-dir DIR] [--no-start]
|
|
25
25
|
${CLI_NAME} install-core-plugin [--no-force]
|
|
26
26
|
${CLI_NAME} install-steer-plugin [--no-force] (compatibility alias)
|
|
27
|
+
${CLI_NAME} check-core-plugin (read-only runtime protocol check)
|
|
27
28
|
${CLI_NAME} doctor [--state-dir DIR]
|
|
28
29
|
${CLI_NAME} --help
|
|
29
30
|
|
|
@@ -250,6 +251,23 @@ function cmdInstallCorePlugin(args) {
|
|
|
250
251
|
console.log(`Load/reload with the Gateway's supported plugin control path, then verify the read-only capability endpoint:`);
|
|
251
252
|
console.log(` openclaw gateway call lma.steer.capabilities --params '{}'`);
|
|
252
253
|
console.log(` (expect protocol:2, durableAttempts:true, acceptanceCallback:true, atomicRunBound:false)`);
|
|
254
|
+
console.log(` ${CLI_NAME} check-core-plugin (read-only validation; generic core capabilities alone are insufficient)`);
|
|
255
|
+
}
|
|
256
|
+
function cmdCheckCorePlugin(args) {
|
|
257
|
+
if (args.length)
|
|
258
|
+
throw new Error(`Unknown check-core-plugin arguments: ${args.join(' ')}`);
|
|
259
|
+
const bin = process.env.OPENCLAW_BIN || 'openclaw';
|
|
260
|
+
let capability;
|
|
261
|
+
try {
|
|
262
|
+
const raw = execFileSync(bin, ['gateway', 'call', 'lma.steer.capabilities', '--params', '{}', '--json'], { encoding: 'utf8', shell: process.platform === 'win32', timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
263
|
+
capability = JSON.parse(raw);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw new Error('lma.steer.capabilities unavailable or invalid: callback/durable insertion is NOT verified. Activate the coordinated lma-core plugin via supported Gateway controls, then recheck.');
|
|
267
|
+
}
|
|
268
|
+
if (capability?.protocol !== 2 || capability?.durableAttempts !== true || capability?.acceptanceCallback !== true || capability?.sessionGenerationBound !== true)
|
|
269
|
+
throw new Error('lma.steer.capabilities incompatible: require protocol=2, durableAttempts=true, acceptanceCallback=true, sessionGenerationBound=true; generic lma.core.capabilities is not activation proof.');
|
|
270
|
+
console.log('OK: runtime lma.steer.capabilities protocol=2; callback acceptance, durable attempts and session generation binding verified (not model consumption or final-effect CAS).');
|
|
253
271
|
}
|
|
254
272
|
function cmdDoctor(args) {
|
|
255
273
|
const stateDir = resolve(takeOption(args, "--state-dir") || DEFAULT_STATE_DIR);
|
|
@@ -277,6 +295,8 @@ async function main() {
|
|
|
277
295
|
return cmdInstallCorePlugin(args);
|
|
278
296
|
if (cmd === "doctor")
|
|
279
297
|
return cmdDoctor(args);
|
|
298
|
+
if (cmd === "check-core-plugin")
|
|
299
|
+
return cmdCheckCorePlugin(args);
|
|
280
300
|
if (cmd === "start") {
|
|
281
301
|
const configPath = args[0] ? resolve(args[0]) : DEFAULT_CONFIG_PATH;
|
|
282
302
|
return startApp(configPath);
|
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -118,6 +118,13 @@ export declare class FeishuBot {
|
|
|
118
118
|
static getByName(name: string): FeishuBot | undefined;
|
|
119
119
|
static findByOpenId(openId: string): FeishuBot | undefined;
|
|
120
120
|
private handleMessage;
|
|
121
|
+
/** With no local owner, the Gateway can still have externally admitted work.
|
|
122
|
+
* Read public activity and pin its exact root before choosing insertion. Missing
|
|
123
|
+
* identity is durable unbound custody, never a fabricated run or blind send. */
|
|
124
|
+
private reconcileCurrentInputActivity;
|
|
125
|
+
/** CAS includes the actual batch, not merely the queue Promise, and excludes
|
|
126
|
+
* local admission/RPC windows even if the host briefly reports idle. */
|
|
127
|
+
private reconcileDispatchCustody;
|
|
121
128
|
private stopGeneration;
|
|
122
129
|
private reactionChecks;
|
|
123
130
|
private receiptReaction;
|
|
@@ -250,6 +257,7 @@ export declare class FeishuBot {
|
|
|
250
257
|
/** Both initial and CONTINUE results enter the same ordinary LMA outbox.
|
|
251
258
|
* No host proactive-send assumption, no fake platform receipt. */
|
|
252
259
|
private deliverGoExecutorResult;
|
|
260
|
+
private insertionNoticeCurrent;
|
|
253
261
|
private dispatchPendingDeliveries;
|
|
254
262
|
private hasFreeModeBot;
|
|
255
263
|
private mentionedBotNames;
|
package/dist/feishu-bot.js
CHANGED
|
@@ -1225,11 +1225,11 @@ export class FeishuBot {
|
|
|
1225
1225
|
// Reserve BEFORE the first health/reaction await. Otherwise an ending
|
|
1226
1226
|
// owner can ordinary-drain this row while its handler is still waiting,
|
|
1227
1227
|
// and the later steer would submit the same message a second time.
|
|
1228
|
-
const ingressOwner = this.queueOwners.get(chatId);
|
|
1229
|
-
if (this.queueRuns.has(chatId) && !ingressOwner?.triggerIds?.has(insertedId)) {
|
|
1228
|
+
const ingressOwner = this.queueOwners.get(chatId) || this.store.getDispatchCustody(this.config.name, chatId);
|
|
1229
|
+
if ((this.queueRuns.has(chatId) || ingressOwner) && !ingressOwner?.triggerIds?.has(insertedId)) {
|
|
1230
1230
|
this.store.saveInsertion(this.config.name, chatId, insertedId, JSON.stringify([this.config.appId, chatId, messageId]), 'pending', {
|
|
1231
1231
|
messageId, text: cleanText, stopEpoch: this.stopGeneration(chatId), expectedRunId: ingressOwner?.runId || '',
|
|
1232
|
-
ownerId: ingressOwner?.id, batchId: ingressOwner?.batchId, sessionKey: this.getSessionKey(chatId)
|
|
1232
|
+
ownerId: ingressOwner?.id, batchId: ingressOwner?.batchId, sessionId: ingressOwner?.sessionId, sessionKey: this.getSessionKey(chatId)
|
|
1233
1233
|
});
|
|
1234
1234
|
reservedAtIngress = true;
|
|
1235
1235
|
}
|
|
@@ -1249,6 +1249,20 @@ export class FeishuBot {
|
|
|
1249
1249
|
if (unhealthy) {
|
|
1250
1250
|
await this.warnUnhealthySessionAndContinue(chatId, messageId, unhealthy);
|
|
1251
1251
|
}
|
|
1252
|
+
// Inspect current public activity, not just the local promise. A yielded
|
|
1253
|
+
// task (or a settled/uncached old root with authoritative idle history)
|
|
1254
|
+
// keeps its result observer but cannot monopolize independent new input.
|
|
1255
|
+
const dispatchReleased = await this.reconcileDispatchCustody(chatId);
|
|
1256
|
+
if (this.stopGeneration(chatId) !== inputStopEpoch)
|
|
1257
|
+
return;
|
|
1258
|
+
if (!isBot && insertedId > 0 && !dispatchReleased && !this.queueRuns.has(chatId) && !this.store.getDispatchCustody(this.config.name, chatId))
|
|
1259
|
+
await this.reconcileCurrentInputActivity(chatId, insertedId, messageId, cleanText);
|
|
1260
|
+
if (this.stopGeneration(chatId) !== inputStopEpoch)
|
|
1261
|
+
return;
|
|
1262
|
+
if (this.store.isInsertionReserved(this.config.name, chatId, insertedId))
|
|
1263
|
+
reservedAtIngress = true;
|
|
1264
|
+
if (reservedAtIngress && !this.store.isInsertionReserved(this.config.name, chatId, insertedId))
|
|
1265
|
+
reservedAtIngress = false;
|
|
1252
1266
|
// Another ingress handler may already have selected/admitted this row
|
|
1253
1267
|
// as an ordinary batch while this handler awaited health. Do not re-steer
|
|
1254
1268
|
// a message that already belongs to that batch.
|
|
@@ -1265,11 +1279,11 @@ export class FeishuBot {
|
|
|
1265
1279
|
// --- Queue-based sending: if agent is busy, just accumulate ---
|
|
1266
1280
|
// The actual queue owner, not elapsed wall time, decides whether input
|
|
1267
1281
|
// should steer/queue. Long healthy runs must not lose their busy ownership.
|
|
1268
|
-
const isBusy = reservedAtIngress || this.queueRuns.has(chatId);
|
|
1282
|
+
const isBusy = reservedAtIngress || this.queueRuns.has(chatId) || !!this.store.getDispatchCustody(this.config.name, chatId);
|
|
1269
1283
|
if (isBusy) {
|
|
1270
|
-
const owner = this.queueOwners.get(chatId);
|
|
1284
|
+
const owner = this.queueOwners.get(chatId) || this.store.getDispatchCustody(this.config.name, chatId);
|
|
1271
1285
|
const record = { messageId, text: cleanText, stopEpoch: this.stopGeneration(chatId),
|
|
1272
|
-
expectedRunId: owner?.runId || '', ownerId: owner?.id, batchId: owner?.batchId, sessionKey: this.getSessionKey(chatId) };
|
|
1286
|
+
expectedRunId: owner?.runId || '', ownerId: owner?.id, batchId: owner?.batchId, sessionId: owner?.sessionId, sessionKey: this.getSessionKey(chatId) };
|
|
1273
1287
|
if (insertedId > 0 && !this.store.getInsertion(this.config.name, chatId, insertedId))
|
|
1274
1288
|
this.store.saveInsertion(this.config.name, chatId, insertedId, JSON.stringify([this.config.appId, chatId, messageId]), 'pending', record);
|
|
1275
1289
|
// Reservation is durable BEFORE any RPC. Ordinary drain cannot race a
|
|
@@ -1279,12 +1293,110 @@ export class FeishuBot {
|
|
|
1279
1293
|
return;
|
|
1280
1294
|
}
|
|
1281
1295
|
// Not busy: admission callback, not local queueing, flips Typing -> Get.
|
|
1282
|
-
await this.processQueue(chatId);
|
|
1296
|
+
await this.processQueue(chatId, true);
|
|
1283
1297
|
}
|
|
1284
1298
|
catch (err) {
|
|
1285
1299
|
console.error(`[${this.config.name}] Error:`, safeErrorSummary(err));
|
|
1286
1300
|
}
|
|
1287
1301
|
}
|
|
1302
|
+
/** With no local owner, the Gateway can still have externally admitted work.
|
|
1303
|
+
* Read public activity and pin its exact root before choosing insertion. Missing
|
|
1304
|
+
* identity is durable unbound custody, never a fabricated run or blind send. */
|
|
1305
|
+
async reconcileCurrentInputActivity(chatId, rowId, messageId, text) {
|
|
1306
|
+
if (!this.openclawClient.getSessionRuntimeStatus || !this.openclawClient.getSessionDispatchStatus)
|
|
1307
|
+
return;
|
|
1308
|
+
const epoch = this.stopGeneration(chatId);
|
|
1309
|
+
// A different ingress handler can ordinary-admit AND finish this row while
|
|
1310
|
+
// a public lookup is in flight. Check message custody, not just an empty
|
|
1311
|
+
// owner map, at every boundary so a late lookup can never reinsert it.
|
|
1312
|
+
const unsubmitted = () => this.stopGeneration(chatId) === epoch &&
|
|
1313
|
+
this.store.getPendingTriggerIds(this.config.name, chatId).has(rowId) &&
|
|
1314
|
+
!this.queueOwners.get(chatId)?.triggerIds?.has(rowId);
|
|
1315
|
+
if (!unsubmitted())
|
|
1316
|
+
return;
|
|
1317
|
+
await this.openclawClient.waitForSessionStop?.(this.getSessionKey(chatId));
|
|
1318
|
+
if (!unsubmitted())
|
|
1319
|
+
return;
|
|
1320
|
+
// Session creation/model setup is not agent admission. Initialize a truly
|
|
1321
|
+
// new chat before asking for its live activity; never infer idle from a
|
|
1322
|
+
// missing describe response. A failed setup is not dispatch-idle evidence;
|
|
1323
|
+
// retain this never-attempted input for paced reconciliation as well.
|
|
1324
|
+
try {
|
|
1325
|
+
await this.ensureSession(chatId);
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
if (unsubmitted() && !this.queueRuns.has(chatId) && !this.store.getDispatchCustody(this.config.name, chatId)) {
|
|
1329
|
+
const row = this.store.getInsertion(this.config.name, chatId, rowId);
|
|
1330
|
+
if (!row || row.state === 'pending' && row.activityUnbound && !row.attempted && !row.expectedRunId)
|
|
1331
|
+
this.store.saveInsertion(this.config.name, chatId, rowId, row?.insertionId || JSON.stringify([this.config.appId, chatId, messageId]), 'pending', {
|
|
1332
|
+
messageId, text, stopEpoch: epoch, sessionKey: this.getSessionKey(chatId), activityUnbound: true, attempted: false, expectedRunId: '',
|
|
1333
|
+
reason: 'session setup unavailable; retained without dispatch-idle proof',
|
|
1334
|
+
}, Date.now() + 5000);
|
|
1335
|
+
}
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
|
|
1339
|
+
return;
|
|
1340
|
+
const status = await this.openclawClient.getSessionRuntimeStatus(this.getSessionKey(chatId));
|
|
1341
|
+
if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
|
|
1342
|
+
return;
|
|
1343
|
+
const current = this.store.getInsertion(this.config.name, chatId, rowId);
|
|
1344
|
+
if (current && (!current.activityUnbound || current.attempted))
|
|
1345
|
+
return;
|
|
1346
|
+
const activity = await this.openclawClient.getSessionDispatchStatus(this.getSessionKey(chatId));
|
|
1347
|
+
if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
|
|
1348
|
+
return;
|
|
1349
|
+
// Another recovery/ingress lookup may have identified the root and begun
|
|
1350
|
+
// dispatch while this one awaited history. Never restore its older
|
|
1351
|
+
// never-attempted snapshot over an attempted/unknown/accepted journal.
|
|
1352
|
+
const fresh = this.store.getInsertion(this.config.name, chatId, rowId);
|
|
1353
|
+
if (current ? fresh?.insertionId !== current.insertionId || fresh.state !== 'pending' || fresh.attempted === true || !fresh.activityUnbound || !!fresh.expectedRunId : !!fresh)
|
|
1354
|
+
return;
|
|
1355
|
+
const compatible = !activity.sessionId || !status.sessionId || activity.sessionId === status.sessionId;
|
|
1356
|
+
const idle = compatible && !status.running && !status.archived && !activity.archived && !activity.busyConfirmed &&
|
|
1357
|
+
activity.idleConfirmed === true && !!activity.sessionId;
|
|
1358
|
+
if (idle) {
|
|
1359
|
+
if (current)
|
|
1360
|
+
this.store.saveInsertion(this.config.name, chatId, rowId, current.insertionId, 'ordinary', { ...current, activityUnbound: false });
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const identified = compatible && !!activity.activeRunId && !!activity.sessionId && !activity.archived && !status.archived;
|
|
1364
|
+
this.store.saveInsertion(this.config.name, chatId, rowId, current?.insertionId || JSON.stringify([this.config.appId, chatId, messageId]), 'pending', {
|
|
1365
|
+
messageId, text, stopEpoch: epoch, sessionKey: this.getSessionKey(chatId), sessionId: identified ? activity.sessionId : status.sessionId,
|
|
1366
|
+
expectedRunId: identified ? activity.activeRunId : '', ownerId: identified ? `external:${activity.activeRunId}` : undefined,
|
|
1367
|
+
batchId: identified ? activity.activeRunId : undefined, activityUnbound: !identified, attempted: false,
|
|
1368
|
+
reason: identified ? 'current Gateway run observed; insertion custody only' : 'current Gateway activity/run identity unavailable; retained without admission',
|
|
1369
|
+
}, Date.now() + (identified ? 0 : 5000));
|
|
1370
|
+
}
|
|
1371
|
+
/** CAS includes the actual batch, not merely the queue Promise, and excludes
|
|
1372
|
+
* local admission/RPC windows even if the host briefly reports idle. */
|
|
1373
|
+
async reconcileDispatchCustody(chatId) {
|
|
1374
|
+
const local = this.queueOwners.get(chatId);
|
|
1375
|
+
const saved = this.store.getDispatchCustody(this.config.name, chatId);
|
|
1376
|
+
if (!saved?.runId || !saved.batchId || !saved.sessionId || !saved.admitted && !saved.yielded)
|
|
1377
|
+
return false;
|
|
1378
|
+
if (local && (local.id !== saved.id || local.batchId !== saved.batchId || !local.admitted && !local.yielded || !local.collecting))
|
|
1379
|
+
return false;
|
|
1380
|
+
const batch = local?.batch, runId = local?.runId, epoch = this.stopGeneration(chatId);
|
|
1381
|
+
const unchanged = () => {
|
|
1382
|
+
const now = this.store.getDispatchCustody(this.config.name, chatId);
|
|
1383
|
+
return this.stopGeneration(chatId) === epoch && now?.id === saved.id && now?.batchId === saved.batchId && now?.runId === saved.runId &&
|
|
1384
|
+
this.queueOwners.get(chatId) === local && (!local || local.batch === batch && local.runId === runId && (local.admitted === true || local.yielded === true) && local.collecting === true);
|
|
1385
|
+
};
|
|
1386
|
+
const activity = await this.openclawClient.getSessionDispatchStatus?.(this.getSessionKey(chatId)).catch(() => null);
|
|
1387
|
+
if (!unchanged() || !activity?.idleConfirmed || activity.busyConfirmed || activity.archived || activity.sessionId !== saved.sessionId)
|
|
1388
|
+
return false;
|
|
1389
|
+
const status = await this.openclawClient.getSessionRuntimeStatus(this.getSessionKey(chatId)).catch(() => null);
|
|
1390
|
+
if (!unchanged() || !status?.idleConfirmed || status.archived || status.sessionId !== saved.sessionId)
|
|
1391
|
+
return false;
|
|
1392
|
+
if (!this.store.releaseDispatchCustody(this.config.name, chatId, saved.id, saved.batchId))
|
|
1393
|
+
return false;
|
|
1394
|
+
// The target was committed on admission and survives this local cleanup.
|
|
1395
|
+
// Only this exact old owner can be revoked; never a replacement batch.
|
|
1396
|
+
if (local)
|
|
1397
|
+
this.forceExpireQueueOwner(chatId, 'current session confirmed schedulable; exact result observation retained');
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1288
1400
|
stopGeneration(chatId) {
|
|
1289
1401
|
return this.stopEpoch.get(chatId) ?? this.store.getStopGeneration(this.config.name, chatId);
|
|
1290
1402
|
}
|
|
@@ -1350,7 +1462,7 @@ export class FeishuBot {
|
|
|
1350
1462
|
this.insertionTimer = setTimeout(() => {
|
|
1351
1463
|
this.insertionTimer = undefined;
|
|
1352
1464
|
const chats = [...new Set([...this.store.listInsertions(this.config.name), ...this.store.listReactionIntents(this.config.name)].map(x => x.chatId))];
|
|
1353
|
-
void Promise.allSettled(chats.
|
|
1465
|
+
void Promise.allSettled(chats.flatMap(chat => [this.reconcileReactions(chat), this.reconcileInsertions(chat)])).finally(() => this.scheduleInsertionRecovery());
|
|
1354
1466
|
}, 5000);
|
|
1355
1467
|
this.insertionTimer.unref?.();
|
|
1356
1468
|
}
|
|
@@ -1359,14 +1471,30 @@ export class FeishuBot {
|
|
|
1359
1471
|
return;
|
|
1360
1472
|
this.insertionChecks.add(chatId);
|
|
1361
1473
|
try {
|
|
1474
|
+
const released = await this.reconcileDispatchCustody(chatId);
|
|
1475
|
+
if (released)
|
|
1476
|
+
void this.processQueue(chatId, true).catch(() => { });
|
|
1362
1477
|
for (const item of this.store.listInsertions(this.config.name, chatId)) {
|
|
1363
1478
|
if (item.nextCheckAt > Date.now())
|
|
1364
1479
|
continue;
|
|
1365
|
-
const stillCurrent = () => this.stopGeneration(chatId) === item.stopEpoch && this.store.getInsertion(this.config.name, chatId, item.rowId)?.state
|
|
1480
|
+
const stillCurrent = () => this.stopGeneration(chatId) === item.stopEpoch && !['stopped', 'ordinary', 'committed'].includes(this.store.getInsertion(this.config.name, chatId, item.rowId)?.state);
|
|
1366
1481
|
if (!stillCurrent()) {
|
|
1367
|
-
this.
|
|
1482
|
+
if (this.stopGeneration(chatId) !== item.stopEpoch)
|
|
1483
|
+
this.store.stopInsertionAtEpoch(this.config.name, chatId, item.rowId, item.stopEpoch);
|
|
1368
1484
|
continue;
|
|
1369
1485
|
}
|
|
1486
|
+
if (item.activityUnbound) {
|
|
1487
|
+
await this.reconcileCurrentInputActivity(chatId, item.rowId, item.messageId, item.text);
|
|
1488
|
+
if (!stillCurrent()) {
|
|
1489
|
+
if (this.store.getInsertion(this.config.name, chatId, item.rowId)?.state === 'ordinary')
|
|
1490
|
+
void this.processQueue(chatId, true).catch(() => { });
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
const refreshed = this.store.getInsertion(this.config.name, chatId, item.rowId);
|
|
1494
|
+
if (!refreshed?.expectedRunId)
|
|
1495
|
+
continue;
|
|
1496
|
+
Object.assign(item, refreshed);
|
|
1497
|
+
}
|
|
1370
1498
|
const owner = this.queueOwners.get(chatId);
|
|
1371
1499
|
if (!item.expectedRunId && owner?.id === item.ownerId && owner?.batchId === item.batchId && owner?.runId)
|
|
1372
1500
|
item.expectedRunId = owner.runId;
|
|
@@ -1377,7 +1505,7 @@ export class FeishuBot {
|
|
|
1377
1505
|
}
|
|
1378
1506
|
// Pin the real session generation BEFORE the dispatching journal. A
|
|
1379
1507
|
// crash/lost response can then query using precisely the same identity.
|
|
1380
|
-
const info = await this.openclawClient.getSessionInfo(item.sessionKey).catch(() => null);
|
|
1508
|
+
const info = await this.openclawClient.getSessionInfo(item.sessionKey, 1500).catch(() => null);
|
|
1381
1509
|
if (!stillCurrent())
|
|
1382
1510
|
continue;
|
|
1383
1511
|
const sessionId = info?.session?.sessionId;
|
|
@@ -1427,19 +1555,20 @@ export class FeishuBot {
|
|
|
1427
1555
|
continue;
|
|
1428
1556
|
const valid = receipt.insertionId === item.insertionId && receipt.expectedRunId === item.expectedRunId && (!receipt.sessionId || receipt.sessionId === item.sessionId);
|
|
1429
1557
|
let state = valid ? receipt.status : 'unknown';
|
|
1430
|
-
// Explicit no-effect
|
|
1431
|
-
// input back
|
|
1432
|
-
//
|
|
1558
|
+
// Explicit no-effect plus confirmed current schedulability can hand
|
|
1559
|
+
// this SAME input back once without claiming old task settlement.
|
|
1560
|
+
// Time elapsed and attempted-unknown receipts never authorize replay.
|
|
1433
1561
|
if (valid && receipt.attempted === false && receipt.ordinaryEligible !== false && ['pending', 'target_ended'].includes(state) &&
|
|
1434
1562
|
['target_not_ready', 'backend_rejected_no_effect', 'original run ended without an insertion attempt'].includes(receipt.reason || '')) {
|
|
1435
1563
|
const status = await this.openclawClient.getSessionRuntimeStatus(item.sessionKey).catch(() => null);
|
|
1436
1564
|
if (!stillCurrent())
|
|
1437
1565
|
continue;
|
|
1438
|
-
const
|
|
1566
|
+
const activity = status?.idleConfirmed ? await this.openclawClient.getSessionDispatchStatus?.(item.sessionKey).catch(() => null) : null;
|
|
1439
1567
|
if (!stillCurrent())
|
|
1440
1568
|
continue;
|
|
1569
|
+
const currentIdle = activity?.idleConfirmed === true && !activity.busyConfirmed && activity.sessionId === item.sessionId && !activity.archived && status?.sessionId === item.sessionId;
|
|
1441
1570
|
const unchanged = this.queueOwners.get(chatId) === owner && owner?.batch === batchAtCheck && owner?.runId === runAtCheck;
|
|
1442
|
-
if (
|
|
1571
|
+
if (currentIdle && status?.idleConfirmed === true && unchanged) {
|
|
1443
1572
|
state = 'ordinary';
|
|
1444
1573
|
// Never expire a successor merely to schedule the old input.
|
|
1445
1574
|
if (runAtCheck === item.expectedRunId)
|
|
@@ -1454,16 +1583,16 @@ export class FeishuBot {
|
|
|
1454
1583
|
state = 'pending';
|
|
1455
1584
|
// Unknown is the NORMAL transient state right after a steer attempt
|
|
1456
1585
|
// (the backend acceptance receipt has not landed yet); surfacing it on
|
|
1457
|
-
// every insertion spams the chat
|
|
1458
|
-
//
|
|
1459
|
-
// re-checks (~45s), then notify exactly once.
|
|
1586
|
+
// every insertion spams the chat. Use real elapsed time from the first
|
|
1587
|
+
// unknown observation, not query count, and commit one reminder intent.
|
|
1460
1588
|
const unknownChecks = state === 'unknown' ? (Number(item.unknownChecks) || 0) + 1 : 0;
|
|
1461
|
-
const record = { ...item, attempted: valid ? (receipt.attempted ?? true) : true, sessionId: item.sessionId, reason: receipt.reason, unknownChecks
|
|
1589
|
+
const record = { ...item, attempted: valid ? (receipt.attempted ?? true) : true, sessionId: item.sessionId, reason: receipt.reason, unknownChecks,
|
|
1590
|
+
unknownSince: state === 'unknown' ? (item.state === 'unknown' && Number.isFinite(item.unknownSince) ? item.unknownSince : Date.now()) : undefined };
|
|
1462
1591
|
this.store.saveInsertion(this.config.name, chatId, item.rowId, item.insertionId, state, record, Date.now() + (state === 'unknown' ? 15_000 : 5000));
|
|
1463
1592
|
if (state === 'ordinary') {
|
|
1464
1593
|
this.store.deletePendingInsertionNotices(this.config.name, chatId, item.insertionId);
|
|
1465
|
-
void this.enqueueAndDispatchDelivery(chatId, 'insertion_status', `insertion:${item.insertionId}:ordinary`, 'ℹ️
|
|
1466
|
-
void this.processQueue(chatId).catch(() => { });
|
|
1594
|
+
void this.enqueueAndDispatchDelivery(chatId, 'insertion_status', `insertion:${item.insertionId}:ordinary`, 'ℹ️ 当前会话已确认可接收新轮次,且这条消息确定未被插入;现已交回普通队列,只执行一次。', [], item.messageId, `insertion:${item.insertionId}:ordinary`).catch(() => { });
|
|
1595
|
+
void this.processQueue(chatId, true).catch(() => { });
|
|
1467
1596
|
}
|
|
1468
1597
|
else if (state === 'accepted') {
|
|
1469
1598
|
this.store.deletePendingInsertionNotices(this.config.name, chatId, item.insertionId);
|
|
@@ -1487,17 +1616,15 @@ export class FeishuBot {
|
|
|
1487
1616
|
if (item.state !== state)
|
|
1488
1617
|
void this.enqueueAndDispatchDelivery(chatId, 'insertion_status', `insertion:${item.insertionId}:${state}`, `ℹ️ 这条插入消息的状态为 ${state},仍保留本地记录,未确认接收前不会显示 Get,也不会自动重复执行。${receipt.reason || ''}`, [], item.messageId, `insertion:${item.insertionId}:${state}`).catch(() => { });
|
|
1489
1618
|
}
|
|
1490
|
-
else if (state === 'unknown' &&
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
.
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
this.
|
|
1499
|
-
})
|
|
1500
|
-
.catch(() => { });
|
|
1619
|
+
else if (state === 'unknown' && Date.now() - record.unknownSince >= 45_000 && !item.unknownNotified) {
|
|
1620
|
+
const text = 'ℹ️ 这条插入消息约 45 秒仍未确认接收:本地记录保留中,不会自动重复执行;确认后会自动显示 Get。';
|
|
1621
|
+
const key = `insertion:${item.insertionId}:unknown-prolonged`;
|
|
1622
|
+
if (this.store.queueInsertionUnknownNotice(this.config.name, chatId, item.rowId, {
|
|
1623
|
+
sessionKey: this.getSessionKey(chatId), chatId, botName: this.config.name, sourceType: 'insertion_status', sourceId: key, deliveryKey: key,
|
|
1624
|
+
contentHash: this.stableHash(`${text}|[]`), content: text, attachmentsJson: '[]', replyToMessageId: item.messageId,
|
|
1625
|
+
deliveryMetaJson: JSON.stringify({ botIdentity: this.config.appId, insertionNotice: { rowId: item.rowId, insertionId: item.insertionId, sessionId: item.sessionId, stopEpoch: item.stopEpoch } }),
|
|
1626
|
+
}))
|
|
1627
|
+
void this.dispatchPendingDeliveries(chatId).catch(() => { });
|
|
1501
1628
|
}
|
|
1502
1629
|
}
|
|
1503
1630
|
}
|
|
@@ -1624,12 +1751,46 @@ export class FeishuBot {
|
|
|
1624
1751
|
catch { /* observer failures are isolated */ }
|
|
1625
1752
|
}
|
|
1626
1753
|
}
|
|
1627
|
-
async processQueue(chatId) {
|
|
1754
|
+
async processQueue(chatId, activityChecked = false) {
|
|
1628
1755
|
const existing = this.queueRuns.get(chatId);
|
|
1629
1756
|
if (existing) {
|
|
1630
1757
|
console.log(`[${this.config.name}] processQueue already running for ${chatId.slice(-8)}, joining existing run`);
|
|
1631
1758
|
return existing;
|
|
1632
1759
|
}
|
|
1760
|
+
const detached = this.store.getDispatchCustody(this.config.name, chatId);
|
|
1761
|
+
if (detached) {
|
|
1762
|
+
// Startup/recovery can reach the queue without an ingress handler. Pin
|
|
1763
|
+
// unsent triggers synchronously before consulting live host activity.
|
|
1764
|
+
for (const message of this.store.getPendingTriggerMessages(this.config.name, chatId)) {
|
|
1765
|
+
if (!message.id || message.senderType !== 'human' || this.store.getInsertion(this.config.name, chatId, message.id))
|
|
1766
|
+
continue;
|
|
1767
|
+
this.store.saveInsertion(this.config.name, chatId, message.id, JSON.stringify([this.config.appId, chatId, message.messageId]), 'pending', {
|
|
1768
|
+
messageId: message.messageId, text: message.content, stopEpoch: this.stopGeneration(chatId), expectedRunId: detached.runId,
|
|
1769
|
+
ownerId: detached.id, batchId: detached.batchId, sessionId: detached.sessionId, sessionKey: this.getSessionKey(chatId),
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
if (await this.reconcileDispatchCustody(chatId))
|
|
1773
|
+
return this.processQueue(chatId, true);
|
|
1774
|
+
void this.reconcileInsertions(chatId).catch(() => { });
|
|
1775
|
+
this.scheduleInsertionRecovery();
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (!activityChecked && typeof this.openclawClient.getSessionRuntimeStatus === 'function' && typeof this.openclawClient.getSessionDispatchStatus === 'function') {
|
|
1779
|
+
const candidates = this.store.getPendingTriggerMessages(this.config.name, chatId)
|
|
1780
|
+
.filter(message => message.id && message.senderType === 'human' && !this.store.isInsertionReserved(this.config.name, chatId, message.id));
|
|
1781
|
+
if (!candidates.length)
|
|
1782
|
+
return;
|
|
1783
|
+
for (const message of candidates)
|
|
1784
|
+
await this.reconcileCurrentInputActivity(chatId, message.id, message.messageId, message.content);
|
|
1785
|
+
// Re-enter through the ordinary owner CAS after all awaits. Any concurrent
|
|
1786
|
+
// admission now owns its batch; never submit in parallel with that owner.
|
|
1787
|
+
const eligible = this.store.getPendingTriggerMessages(this.config.name, chatId).some(message => message.id && message.senderType === 'human' && !this.store.isInsertionReserved(this.config.name, chatId, message.id));
|
|
1788
|
+
if (eligible)
|
|
1789
|
+
return this.processQueue(chatId, true);
|
|
1790
|
+
void this.reconcileInsertions(chatId).catch(() => { });
|
|
1791
|
+
this.scheduleInsertionRecovery();
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1633
1794
|
const owner = { id: randomUUID(), token: {}, revoked: false, stopEpoch: this.stopGeneration(chatId) };
|
|
1634
1795
|
this.queueOwners.set(chatId, owner);
|
|
1635
1796
|
const run = this.processQueueInner(chatId, owner).finally(() => {
|
|
@@ -1655,13 +1816,16 @@ export class FeishuBot {
|
|
|
1655
1816
|
* the proactive path are invisible to them. Steer-fallback triggers are
|
|
1656
1817
|
* restored via the failure observers so their input drains normally.
|
|
1657
1818
|
*/
|
|
1658
|
-
forceExpireQueueOwner(chatId, reason) {
|
|
1819
|
+
forceExpireQueueOwner(chatId, reason, preserveDispatch = false) {
|
|
1659
1820
|
const owner = this.queueRuns.get(chatId);
|
|
1660
1821
|
if (!owner)
|
|
1661
1822
|
return false;
|
|
1662
1823
|
const context = this.queueOwners.get(chatId);
|
|
1824
|
+
if (context?.batchId && !preserveDispatch)
|
|
1825
|
+
this.store.clearDispatchCustody(this.config.name, chatId, context.id, context.batchId);
|
|
1663
1826
|
if (context)
|
|
1664
1827
|
context.revoked = true;
|
|
1828
|
+
this.busyChats.set(chatId, 0);
|
|
1665
1829
|
console.warn(`[${this.config.name}] force-expiring zombie queue owner for ${chatId.slice(-8)}: ${reason}`);
|
|
1666
1830
|
// Fence the stale batch's local observation BEFORE releasing custody: an
|
|
1667
1831
|
// alive old poller would keep destructively consuming the replacement
|
|
@@ -1689,6 +1853,10 @@ export class FeishuBot {
|
|
|
1689
1853
|
owner.batchId = randomUUID();
|
|
1690
1854
|
owner.triggerIds = undefined;
|
|
1691
1855
|
owner.runId = undefined;
|
|
1856
|
+
owner.admitted = false;
|
|
1857
|
+
owner.collecting = false;
|
|
1858
|
+
owner.yielded = false;
|
|
1859
|
+
owner.sessionId = undefined;
|
|
1692
1860
|
this.queueRunTokens.set(chatId, batchToken);
|
|
1693
1861
|
// A fresh batch starts: steered inputs admitted into the PREVIOUS batch
|
|
1694
1862
|
// completed with it (consumed or not); their failure observers are stale.
|
|
@@ -1846,6 +2014,9 @@ export class FeishuBot {
|
|
|
1846
2014
|
try {
|
|
1847
2015
|
sessionKey = await this.ensureSession(chatId);
|
|
1848
2016
|
guard();
|
|
2017
|
+
const generation = await this.openclawClient.getSessionInfo(sessionKey, 1500).catch(() => null);
|
|
2018
|
+
guard();
|
|
2019
|
+
owner.sessionId = generation?.session?.sessionId;
|
|
1849
2020
|
}
|
|
1850
2021
|
catch (err) {
|
|
1851
2022
|
if (!current())
|
|
@@ -1933,11 +2104,14 @@ export class FeishuBot {
|
|
|
1933
2104
|
// OpenClaw accepts it, retrying would duplicate the same message.
|
|
1934
2105
|
markSubmittedBatch("send-attempt");
|
|
1935
2106
|
},
|
|
1936
|
-
onRunIdentified: (runId) => { guard(); owner.runId = runId; foregroundRunId = runId; },
|
|
2107
|
+
onRunIdentified: (runId, identity) => { guard(); owner.runId = runId; owner.sessionId = identity?.sessionId || owner.sessionId; foregroundRunId = runId; },
|
|
1937
2108
|
onSubmitted: async (runId) => {
|
|
1938
2109
|
guard();
|
|
1939
2110
|
owner.runId = runId;
|
|
2111
|
+
owner.admitted = true;
|
|
2112
|
+
this.store.saveDispatchCustody(this.config.name, chatId, { id: owner.id, batchId: owner.batchId, runId, sessionId: owner.sessionId, admitted: true, stopEpoch: owner.stopEpoch, sessionKey });
|
|
1940
2113
|
void this.reconcileInsertions(chatId).catch(() => { });
|
|
2114
|
+
this.bindRunDeliveryTarget(chatId, runId, { triggerId, messageId: lastHuman.messageId || '', liveStatus });
|
|
1941
2115
|
for (const ack of this.pendingAckMessages.get(chatId) || []) {
|
|
1942
2116
|
if (!mergedTriggerIdSet.has(ack.rowId))
|
|
1943
2117
|
continue;
|
|
@@ -1960,6 +2134,21 @@ export class FeishuBot {
|
|
|
1960
2134
|
markSubmittedBatch(`submitted:${runId}`);
|
|
1961
2135
|
},
|
|
1962
2136
|
onProgress: (event) => current() ? liveStatus?.progress(event) : undefined,
|
|
2137
|
+
onCollectionStarted: runId => { guard(); if (owner.runId === runId)
|
|
2138
|
+
owner.collecting = true; },
|
|
2139
|
+
onYieldHandoff: runId => {
|
|
2140
|
+
guard();
|
|
2141
|
+
if (owner.runId !== runId || !owner.batchId)
|
|
2142
|
+
throw new Error('Yield root/batch identity mismatch');
|
|
2143
|
+
// All durable result routing commits synchronously before the
|
|
2144
|
+
// collector is fenced and before another input may take custody.
|
|
2145
|
+
this.bindRunDeliveryTarget(chatId, runId, { triggerId, messageId: lastHuman.messageId || '', liveStatus });
|
|
2146
|
+
owner.yielded = true;
|
|
2147
|
+
this.store.saveDispatchCustody(this.config.name, chatId, { id: owner.id, batchId: owner.batchId, runId, sessionId: owner.sessionId, admitted: owner.admitted === true, yielded: true, stopEpoch: owner.stopEpoch, sessionKey });
|
|
2148
|
+
this.forceExpireQueueOwner(chatId, 'trusted exact-root yield; background result custody retained', true);
|
|
2149
|
+
void this.reconcileInsertions(chatId).catch(() => { });
|
|
2150
|
+
this.scheduleInsertionRecovery();
|
|
2151
|
+
},
|
|
1963
2152
|
onWaitPaused: async (notice) => {
|
|
1964
2153
|
guard();
|
|
1965
2154
|
await liveStatus?.showWaitingForResult(notice.message);
|
|
@@ -2199,6 +2388,10 @@ export class FeishuBot {
|
|
|
2199
2388
|
}
|
|
2200
2389
|
catch (err) {
|
|
2201
2390
|
if (!current() || err instanceof SupersededObservation) {
|
|
2391
|
+
if (current()) {
|
|
2392
|
+
this.busyChats.set(chatId, 0);
|
|
2393
|
+
owner.revoked = true;
|
|
2394
|
+
}
|
|
2202
2395
|
// This batch's local observation was fenced by a newer batch (or a
|
|
2203
2396
|
// custody revocation). The new owner handles delivery from here; end
|
|
2204
2397
|
// quietly — no error notice, no replay, no Gateway abort. The old
|
|
@@ -2305,8 +2498,11 @@ export class FeishuBot {
|
|
|
2305
2498
|
break;
|
|
2306
2499
|
}
|
|
2307
2500
|
finally {
|
|
2308
|
-
if (current())
|
|
2501
|
+
if (current()) {
|
|
2309
2502
|
this.busyChats.set(chatId, 0);
|
|
2503
|
+
if (owner.batchId && !owner.yielded)
|
|
2504
|
+
this.store.clearDispatchCustody(this.config.name, chatId, owner.id, owner.batchId);
|
|
2505
|
+
}
|
|
2310
2506
|
}
|
|
2311
2507
|
// Check if more messages arrived while we were busy
|
|
2312
2508
|
// Small delay to let any in-flight messages settle
|
|
@@ -2803,7 +2999,7 @@ export class FeishuBot {
|
|
|
2803
2999
|
const previous = this.deliveryTargetsByRun.get(key);
|
|
2804
3000
|
if (previous?.timer)
|
|
2805
3001
|
clearTimeout(previous.timer);
|
|
2806
|
-
this.store.bindDeliveryRun(this.config.name, chatId, runId, { triggerId: target.triggerId, messageId: target.messageId, runId, sessionKey: `agent:main:${this.getSessionKey(chatId).replace(/^agent:[^:]+:/, '')}` });
|
|
3002
|
+
this.store.bindDeliveryRun(this.config.name, chatId, runId, { ...this.store.getDeliveryRunTarget(this.config.name, chatId, runId), triggerId: target.triggerId, messageId: target.messageId, runId, stopGeneration: this.stopGeneration(chatId), sessionKey: `agent:main:${this.getSessionKey(chatId).replace(/^agent:[^:]+:/, '')}` });
|
|
2807
3003
|
this.deliveryTargetsByRun.set(key, {
|
|
2808
3004
|
triggerId: target.triggerId,
|
|
2809
3005
|
messageId: target.messageId,
|
|
@@ -3162,7 +3358,7 @@ export class FeishuBot {
|
|
|
3162
3358
|
this.store.saveErrorNotice(record);
|
|
3163
3359
|
if (verdict.state === "terminal" && verdict.terminalKey && verdict.text) {
|
|
3164
3360
|
// Independent identity: never take the result's trigger/final key.
|
|
3165
|
-
await this.enqueueAndDispatchDelivery(chatId, "run_error", verdict.terminalKey, verdict.text, [], undefined, verdict.terminalKey, undefined, { sourceType: "run_error", runId: record.provenance.runId, error: record.provenance, terminalEvidence: verdict }, record.noticeId);
|
|
3361
|
+
await this.enqueueAndDispatchDelivery(chatId, "run_error", verdict.terminalKey, verdict.text, [], undefined, verdict.terminalKey, undefined, { sourceType: "run_error", sessionKey: record.provenance.sessionKey, runId: record.provenance.runId, error: record.provenance, terminalEvidence: verdict }, record.noticeId);
|
|
3166
3362
|
// The send-time recheck may have parked or suppressed the observation.
|
|
3167
3363
|
const refreshed = this.store.getErrorNotice(this.config.name, chatId, record.noticeId);
|
|
3168
3364
|
if (refreshed?.verdict && ["pending", "suppressed", "stopped"].includes(refreshed.verdict.state))
|
|
@@ -3212,6 +3408,14 @@ export class FeishuBot {
|
|
|
3212
3408
|
if (this.store.getDeliveryByKey(this.config.name, chatId, source)?.status !== "delivered")
|
|
3213
3409
|
throw new Error("Go executor outbox delivery pending/unconfirmed");
|
|
3214
3410
|
}
|
|
3411
|
+
insertionNoticeCurrent(chatId, notice) {
|
|
3412
|
+
if (!notice)
|
|
3413
|
+
return true;
|
|
3414
|
+
const current = this.store.getInsertion(this.config.name, chatId, notice.rowId);
|
|
3415
|
+
return current?.insertionId === notice.insertionId && current.state === 'unknown' && current.stopEpoch === notice.stopEpoch &&
|
|
3416
|
+
this.stopGeneration(chatId) === notice.stopEpoch && current.sessionId === notice.sessionId &&
|
|
3417
|
+
Number.isFinite(current.unknownSince) && Date.now() - current.unknownSince >= 45_000;
|
|
3418
|
+
}
|
|
3215
3419
|
async dispatchPendingDeliveries(chatId, replyToMessageId) {
|
|
3216
3420
|
this.installGoReceiptCapture();
|
|
3217
3421
|
for (const id of this.localDeliveryRetryCustody.get(chatId) || []) {
|
|
@@ -3230,6 +3434,20 @@ export class FeishuBot {
|
|
|
3230
3434
|
if (pending.length === 0)
|
|
3231
3435
|
break;
|
|
3232
3436
|
for (const item of pending) {
|
|
3437
|
+
const notice = JSON.parse(item.deliveryMetaJson || '{}').insertionNotice;
|
|
3438
|
+
if (notice) {
|
|
3439
|
+
const info = await this.openclawClient.getSessionInfo(this.getSessionKey(chatId), 1500).catch(() => null);
|
|
3440
|
+
if (!this.insertionNoticeCurrent(chatId, notice) || info?.session?.archived === true ||
|
|
3441
|
+
info?.session?.sessionId && info.session.sessionId !== notice.sessionId) {
|
|
3442
|
+
this.store.markDeliveryFailed(item.id);
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
if (!info?.session?.sessionId) {
|
|
3446
|
+
this.scheduleDeliveryRetry(chatId, 1);
|
|
3447
|
+
retryScheduled = true;
|
|
3448
|
+
continue;
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3233
3451
|
const ownerId = JSON.parse(item.deliveryMetaJson || "{}").provenance?.goJobId;
|
|
3234
3452
|
const deliveryJob = ownerId ? this.store.getGoJobById(ownerId) : this.store.getActiveGoJob(this.config.name, chatId);
|
|
3235
3453
|
const receipts = [
|
|
@@ -3238,6 +3456,8 @@ export class FeishuBot {
|
|
|
3238
3456
|
];
|
|
3239
3457
|
await this.sendOrdered(chatId, () => this.goDeliveryReceipts.run({ receipts, goId: deliveryJob?.id, outboxId: item.id, chatId, stage: "body", ordinals: new Map(), effectGuard: () => {
|
|
3240
3458
|
const meta = JSON.parse(item.deliveryMetaJson || '{}'), p = meta.provenance;
|
|
3459
|
+
if (!this.insertionNoticeCurrent(chatId, meta.insertionNotice))
|
|
3460
|
+
throw Object.assign(new Error('Insertion notice no longer current'), { deliveryStopped: true });
|
|
3241
3461
|
if ((p?.final || item.sourceType === 'run_error') &&
|
|
3242
3462
|
(typeof meta.deliveryStopEpoch === 'number' && meta.deliveryStopEpoch !== this.stopGeneration(chatId) ||
|
|
3243
3463
|
p?.sessionKey && p.runId && this.store.isStoppedObservation(p.sessionKey, p.runId))) {
|
|
@@ -3380,6 +3600,12 @@ export class FeishuBot {
|
|
|
3380
3600
|
}
|
|
3381
3601
|
}
|
|
3382
3602
|
this.store.markDeliveryDelivered(item.id);
|
|
3603
|
+
if (item.sourceType === 'run_error' && provenance?.terminalEvidence?.state === 'terminal' && provenance.runId && !provenance.runId.startsWith('announce:')) {
|
|
3604
|
+
this.store.settleRunReactionIntents(this.config.name, chatId, provenance.runId);
|
|
3605
|
+
if (provenance.sessionKey)
|
|
3606
|
+
this.store.finishRunObservation(provenance.sessionKey, provenance.runId);
|
|
3607
|
+
void this.reconcileReactions(chatId).catch(() => { });
|
|
3608
|
+
}
|
|
3383
3609
|
if (provenance?.final && provenance.runId) {
|
|
3384
3610
|
this.store.settleRunReactionIntents(this.config.name, chatId, provenance.runId);
|
|
3385
3611
|
void this.reconcileReactions(chatId).catch(() => { });
|
|
@@ -4887,7 +5113,10 @@ export class FeishuBot {
|
|
|
4887
5113
|
this.store.stopInsertions(this.config.name, chatId);
|
|
4888
5114
|
this.store.cancelReactionIntents(this.config.name, chatId, this.stopGeneration(chatId));
|
|
4889
5115
|
void this.openclawClient.cancelSteerSession?.(sessionKey, this.stopGeneration(chatId)).catch(() => { });
|
|
4890
|
-
|
|
5116
|
+
this.forceExpireQueueOwner(chatId, 'explicit session reset');
|
|
5117
|
+
this.store.clearAllPendingTriggers(this.config.name, chatId);
|
|
5118
|
+
// Fire reset (no response expected). Client fences old exact observations
|
|
5119
|
+
// and exposes its reset barrier to new post-cutoff human inputs.
|
|
4891
5120
|
this.openclawClient.resetSession(sessionKey).catch(() => { });
|
|
4892
5121
|
// Wait a moment for it to take effect
|
|
4893
5122
|
await new Promise((r) => setTimeout(r, 2000));
|
package/dist/message-store.d.ts
CHANGED
|
@@ -114,6 +114,13 @@ export declare class MessageStore {
|
|
|
114
114
|
listReactionIntents(bot: string, chat?: string): any[];
|
|
115
115
|
bindDeliveryRun(bot: string, chat: string, runId: string, record: object): void;
|
|
116
116
|
getDeliveryRunTarget(bot: string, chat: string, runId: string): any;
|
|
117
|
+
/** Scheduling custody is independent of exact-run result custody. */
|
|
118
|
+
saveDispatchCustody(bot: string, chat: string, record: object): void;
|
|
119
|
+
getDispatchCustody(bot: string, chat: string): any;
|
|
120
|
+
clearDispatchCustody(bot: string, chat: string, ownerId: string, batchId: string): boolean;
|
|
121
|
+
/** Atomic release of proven never-attempted reservations only. Unknown and
|
|
122
|
+
* accepted inputs retain their exact target and never enter ordinary drain. */
|
|
123
|
+
releaseDispatchCustody(bot: string, chat: string, ownerId: string, batchId: string): boolean;
|
|
117
124
|
/** Reserved is persisted before the send-attempt callback; dispatching is
|
|
118
125
|
* persisted before the RPC. Only reserved + unchanged stop generation proves
|
|
119
126
|
* no business effect and may be restored on startup. Unknown never replays. */
|
|
@@ -125,6 +132,8 @@ export declare class MessageStore {
|
|
|
125
132
|
settleRunReactionIntents(bot: string, chat: string, runId: string): void;
|
|
126
133
|
cancelReactionIntents(bot: string, chat: string, epoch: number): void;
|
|
127
134
|
saveRunObservation(sessionKey: string, runId: string, record: object, nextCheckAt?: number): void;
|
|
135
|
+
hasYieldedObservation(sessionKey: string): boolean;
|
|
136
|
+
getRunObservation(sessionKey: string, runId: string): any;
|
|
128
137
|
runObservationState(sessionKey: string, runId: string): string | undefined;
|
|
129
138
|
stopSessionObservations(sessionKey: string): void;
|
|
130
139
|
settleExactRunReactions(sessionKey: string, runId: string): void;
|
|
@@ -134,10 +143,14 @@ export declare class MessageStore {
|
|
|
134
143
|
/** Purge still-pending insertion status notices once the insertion resolves
|
|
135
144
|
* (accepted/ordinary): a stale "unknown" notice must not fire after Get. */
|
|
136
145
|
deletePendingInsertionNotices(bot: string, chat: string, insertionId: string): number;
|
|
146
|
+
/** Reminder intent and its outbox row commit together. Never write a stale
|
|
147
|
+
* reconciliation snapshot over a later accepted/stopped insertion. */
|
|
148
|
+
queueInsertionUnknownNotice(bot: string, chat: string, row: number, delivery: Omit<DeliveryOutboxItem, 'id' | 'status' | 'attempts' | 'createdAt' | 'updatedAt'>): boolean;
|
|
137
149
|
claimInsertionAttempt(bot: string, chat: string, row: number): boolean;
|
|
138
150
|
getInsertion(bot: string, chat: string, row: number): any;
|
|
139
151
|
listInsertions(bot: string, chat?: string): any[];
|
|
140
152
|
isInsertionReserved(bot: string, chat: string, row: number): boolean;
|
|
153
|
+
stopInsertionAtEpoch(bot: string, chat: string, row: number, epoch: number): void;
|
|
141
154
|
stopInsertions(bot: string, chat: string): void;
|
|
142
155
|
hasUnknownDeliveryOperation(outbox: number): boolean;
|
|
143
156
|
/** Retain the full known result BEFORE the fallible main-journal checkpoint.
|