openclaw-lark-multi-agent 1.8.8 → 1.8.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/dist/cli.js +20 -0
- package/dist/feishu-bot.d.ts +8 -0
- package/dist/feishu-bot.js +269 -31
- package/dist/message-store.d.ts +16 -0
- package/dist/message-store.js +68 -1
- package/dist/openclaw-client.d.ts +32 -6
- package/dist/openclaw-client.js +224 -60
- 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/package.json +4 -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,103 @@ 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. Setup errors keep the existing queue error path.
|
|
1323
|
+
try {
|
|
1324
|
+
await this.ensureSession(chatId);
|
|
1325
|
+
}
|
|
1326
|
+
catch {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
|
|
1330
|
+
return;
|
|
1331
|
+
const status = await this.openclawClient.getSessionRuntimeStatus(this.getSessionKey(chatId));
|
|
1332
|
+
if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
|
|
1333
|
+
return;
|
|
1334
|
+
const current = this.store.getInsertion(this.config.name, chatId, rowId);
|
|
1335
|
+
if (current && (!current.activityUnbound || current.attempted))
|
|
1336
|
+
return;
|
|
1337
|
+
const activity = await this.openclawClient.getSessionDispatchStatus(this.getSessionKey(chatId));
|
|
1338
|
+
if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
|
|
1339
|
+
return;
|
|
1340
|
+
// Another recovery/ingress lookup may have identified the root and begun
|
|
1341
|
+
// dispatch while this one awaited history. Never restore its older
|
|
1342
|
+
// never-attempted snapshot over an attempted/unknown/accepted journal.
|
|
1343
|
+
const fresh = this.store.getInsertion(this.config.name, chatId, rowId);
|
|
1344
|
+
if (current ? fresh?.insertionId !== current.insertionId || fresh.state !== 'pending' || fresh.attempted === true || !fresh.activityUnbound || !!fresh.expectedRunId : !!fresh)
|
|
1345
|
+
return;
|
|
1346
|
+
const compatible = !activity.sessionId || !status.sessionId || activity.sessionId === status.sessionId;
|
|
1347
|
+
const idle = compatible && !status.archived && !activity.archived && !activity.busyConfirmed &&
|
|
1348
|
+
(status.idleConfirmed && !!status.sessionId || activity.idleConfirmed && !!activity.sessionId);
|
|
1349
|
+
if (idle) {
|
|
1350
|
+
if (current)
|
|
1351
|
+
this.store.saveInsertion(this.config.name, chatId, rowId, current.insertionId, 'ordinary', { ...current, activityUnbound: false });
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const identified = compatible && !!activity.activeRunId && !!activity.sessionId && !activity.archived && !status.archived;
|
|
1355
|
+
this.store.saveInsertion(this.config.name, chatId, rowId, current?.insertionId || JSON.stringify([this.config.appId, chatId, messageId]), 'pending', {
|
|
1356
|
+
messageId, text, stopEpoch: epoch, sessionKey: this.getSessionKey(chatId), sessionId: identified ? activity.sessionId : status.sessionId,
|
|
1357
|
+
expectedRunId: identified ? activity.activeRunId : '', ownerId: identified ? `external:${activity.activeRunId}` : undefined,
|
|
1358
|
+
batchId: identified ? activity.activeRunId : undefined, activityUnbound: !identified, attempted: false,
|
|
1359
|
+
reason: identified ? 'current Gateway run observed; insertion custody only' : 'current Gateway activity/run identity unavailable; retained without admission',
|
|
1360
|
+
}, Date.now() + (identified ? 0 : 5000));
|
|
1361
|
+
}
|
|
1362
|
+
/** CAS includes the actual batch, not merely the queue Promise, and excludes
|
|
1363
|
+
* local admission/RPC windows even if the host briefly reports idle. */
|
|
1364
|
+
async reconcileDispatchCustody(chatId) {
|
|
1365
|
+
const local = this.queueOwners.get(chatId);
|
|
1366
|
+
const saved = this.store.getDispatchCustody(this.config.name, chatId);
|
|
1367
|
+
if (!saved?.runId || !saved.batchId || !saved.sessionId || !saved.admitted && !saved.yielded)
|
|
1368
|
+
return false;
|
|
1369
|
+
if (local && (local.id !== saved.id || local.batchId !== saved.batchId || !local.admitted && !local.yielded || !local.collecting))
|
|
1370
|
+
return false;
|
|
1371
|
+
const batch = local?.batch, runId = local?.runId, epoch = this.stopGeneration(chatId);
|
|
1372
|
+
const unchanged = () => {
|
|
1373
|
+
const now = this.store.getDispatchCustody(this.config.name, chatId);
|
|
1374
|
+
return this.stopGeneration(chatId) === epoch && now?.id === saved.id && now?.batchId === saved.batchId && now?.runId === saved.runId &&
|
|
1375
|
+
this.queueOwners.get(chatId) === local && (!local || local.batch === batch && local.runId === runId && (local.admitted === true || local.yielded === true) && local.collecting === true);
|
|
1376
|
+
};
|
|
1377
|
+
if (!saved.yielded) {
|
|
1378
|
+
const activity = await this.openclawClient.getSessionDispatchStatus?.(this.getSessionKey(chatId)).catch(() => null);
|
|
1379
|
+
if (!unchanged() || !activity?.idleConfirmed || activity.archived || activity.sessionId !== saved.sessionId)
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
const status = await this.openclawClient.getSessionRuntimeStatus(this.getSessionKey(chatId)).catch(() => null);
|
|
1383
|
+
if (!unchanged() || !status?.idleConfirmed || status.archived || status.sessionId !== saved.sessionId)
|
|
1384
|
+
return false;
|
|
1385
|
+
if (!this.store.releaseDispatchCustody(this.config.name, chatId, saved.id, saved.batchId))
|
|
1386
|
+
return false;
|
|
1387
|
+
// The target was committed on admission and survives this local cleanup.
|
|
1388
|
+
// Only this exact old owner can be revoked; never a replacement batch.
|
|
1389
|
+
if (local)
|
|
1390
|
+
this.forceExpireQueueOwner(chatId, 'current session confirmed schedulable; exact result observation retained');
|
|
1391
|
+
return true;
|
|
1392
|
+
}
|
|
1288
1393
|
stopGeneration(chatId) {
|
|
1289
1394
|
return this.stopEpoch.get(chatId) ?? this.store.getStopGeneration(this.config.name, chatId);
|
|
1290
1395
|
}
|
|
@@ -1350,7 +1455,7 @@ export class FeishuBot {
|
|
|
1350
1455
|
this.insertionTimer = setTimeout(() => {
|
|
1351
1456
|
this.insertionTimer = undefined;
|
|
1352
1457
|
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.
|
|
1458
|
+
void Promise.allSettled(chats.flatMap(chat => [this.reconcileReactions(chat), this.reconcileInsertions(chat)])).finally(() => this.scheduleInsertionRecovery());
|
|
1354
1459
|
}, 5000);
|
|
1355
1460
|
this.insertionTimer.unref?.();
|
|
1356
1461
|
}
|
|
@@ -1359,14 +1464,30 @@ export class FeishuBot {
|
|
|
1359
1464
|
return;
|
|
1360
1465
|
this.insertionChecks.add(chatId);
|
|
1361
1466
|
try {
|
|
1467
|
+
const released = await this.reconcileDispatchCustody(chatId);
|
|
1468
|
+
if (released)
|
|
1469
|
+
void this.processQueue(chatId, true).catch(() => { });
|
|
1362
1470
|
for (const item of this.store.listInsertions(this.config.name, chatId)) {
|
|
1363
1471
|
if (item.nextCheckAt > Date.now())
|
|
1364
1472
|
continue;
|
|
1365
|
-
const stillCurrent = () => this.stopGeneration(chatId) === item.stopEpoch && this.store.getInsertion(this.config.name, chatId, item.rowId)?.state
|
|
1473
|
+
const stillCurrent = () => this.stopGeneration(chatId) === item.stopEpoch && !['stopped', 'ordinary', 'committed'].includes(this.store.getInsertion(this.config.name, chatId, item.rowId)?.state);
|
|
1366
1474
|
if (!stillCurrent()) {
|
|
1367
|
-
this.
|
|
1475
|
+
if (this.stopGeneration(chatId) !== item.stopEpoch)
|
|
1476
|
+
this.store.stopInsertionAtEpoch(this.config.name, chatId, item.rowId, item.stopEpoch);
|
|
1368
1477
|
continue;
|
|
1369
1478
|
}
|
|
1479
|
+
if (item.activityUnbound) {
|
|
1480
|
+
await this.reconcileCurrentInputActivity(chatId, item.rowId, item.messageId, item.text);
|
|
1481
|
+
if (!stillCurrent()) {
|
|
1482
|
+
if (this.store.getInsertion(this.config.name, chatId, item.rowId)?.state === 'ordinary')
|
|
1483
|
+
void this.processQueue(chatId, true).catch(() => { });
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const refreshed = this.store.getInsertion(this.config.name, chatId, item.rowId);
|
|
1487
|
+
if (!refreshed?.expectedRunId)
|
|
1488
|
+
continue;
|
|
1489
|
+
Object.assign(item, refreshed);
|
|
1490
|
+
}
|
|
1370
1491
|
const owner = this.queueOwners.get(chatId);
|
|
1371
1492
|
if (!item.expectedRunId && owner?.id === item.ownerId && owner?.batchId === item.batchId && owner?.runId)
|
|
1372
1493
|
item.expectedRunId = owner.runId;
|
|
@@ -1377,7 +1498,7 @@ export class FeishuBot {
|
|
|
1377
1498
|
}
|
|
1378
1499
|
// Pin the real session generation BEFORE the dispatching journal. A
|
|
1379
1500
|
// crash/lost response can then query using precisely the same identity.
|
|
1380
|
-
const info = await this.openclawClient.getSessionInfo(item.sessionKey).catch(() => null);
|
|
1501
|
+
const info = await this.openclawClient.getSessionInfo(item.sessionKey, 1500).catch(() => null);
|
|
1381
1502
|
if (!stillCurrent())
|
|
1382
1503
|
continue;
|
|
1383
1504
|
const sessionId = info?.session?.sessionId;
|
|
@@ -1427,9 +1548,9 @@ export class FeishuBot {
|
|
|
1427
1548
|
continue;
|
|
1428
1549
|
const valid = receipt.insertionId === item.insertionId && receipt.expectedRunId === item.expectedRunId && (!receipt.sessionId || receipt.sessionId === item.sessionId);
|
|
1429
1550
|
let state = valid ? receipt.status : 'unknown';
|
|
1430
|
-
// Explicit no-effect
|
|
1431
|
-
// input back
|
|
1432
|
-
//
|
|
1551
|
+
// Explicit no-effect plus confirmed current schedulability can hand
|
|
1552
|
+
// this SAME input back once without claiming old task settlement.
|
|
1553
|
+
// Time elapsed and attempted-unknown receipts never authorize replay.
|
|
1433
1554
|
if (valid && receipt.attempted === false && receipt.ordinaryEligible !== false && ['pending', 'target_ended'].includes(state) &&
|
|
1434
1555
|
['target_not_ready', 'backend_rejected_no_effect', 'original run ended without an insertion attempt'].includes(receipt.reason || '')) {
|
|
1435
1556
|
const status = await this.openclawClient.getSessionRuntimeStatus(item.sessionKey).catch(() => null);
|
|
@@ -1438,8 +1559,15 @@ export class FeishuBot {
|
|
|
1438
1559
|
const rootSettled = await this.openclawClient.exactRunSettled?.(item.expectedRunId);
|
|
1439
1560
|
if (!stillCurrent())
|
|
1440
1561
|
continue;
|
|
1562
|
+
const activity = !owner && !rootSettled && status?.idleConfirmed ? await this.openclawClient.getSessionDispatchStatus?.(item.sessionKey).catch(() => null) : null;
|
|
1563
|
+
if (!stillCurrent())
|
|
1564
|
+
continue;
|
|
1565
|
+
const currentIdle = activity?.idleConfirmed === true && activity.sessionId === item.sessionId && !activity.archived && status?.sessionId === item.sessionId;
|
|
1441
1566
|
const unchanged = this.queueOwners.get(chatId) === owner && owner?.batch === batchAtCheck && owner?.runId === runAtCheck;
|
|
1442
|
-
|
|
1567
|
+
const target = this.store.getDeliveryRunTarget(this.config.name, chatId, item.expectedRunId);
|
|
1568
|
+
const dispatchReleased = target?.dispatchReleased === true && target.dispatchOwnerId === item.ownerId && target.dispatchBatchId === item.batchId &&
|
|
1569
|
+
target.sessionId === item.sessionId && status?.sessionId === item.sessionId && !status?.archived;
|
|
1570
|
+
if ((rootSettled === true || dispatchReleased || currentIdle) && status?.idleConfirmed === true && unchanged) {
|
|
1443
1571
|
state = 'ordinary';
|
|
1444
1572
|
// Never expire a successor merely to schedule the old input.
|
|
1445
1573
|
if (runAtCheck === item.expectedRunId)
|
|
@@ -1454,18 +1582,19 @@ export class FeishuBot {
|
|
|
1454
1582
|
state = 'pending';
|
|
1455
1583
|
// Unknown is the NORMAL transient state right after a steer attempt
|
|
1456
1584
|
// (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.
|
|
1585
|
+
// every insertion spams the chat. Use real elapsed time from the first
|
|
1586
|
+
// unknown observation, not query count, and commit one reminder intent.
|
|
1460
1587
|
const unknownChecks = state === 'unknown' ? (Number(item.unknownChecks) || 0) + 1 : 0;
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1588
|
+
const record = { ...item, attempted: valid ? (receipt.attempted ?? true) : true, sessionId: item.sessionId, reason: receipt.reason, unknownChecks,
|
|
1589
|
+
unknownSince: state === 'unknown' ? (item.state === 'unknown' && Number.isFinite(item.unknownSince) ? item.unknownSince : Date.now()) : undefined };
|
|
1463
1590
|
this.store.saveInsertion(this.config.name, chatId, item.rowId, item.insertionId, state, record, Date.now() + (state === 'unknown' ? 15_000 : 5000));
|
|
1464
1591
|
if (state === 'ordinary') {
|
|
1465
|
-
|
|
1466
|
-
void this.
|
|
1592
|
+
this.store.deletePendingInsertionNotices(this.config.name, chatId, item.insertionId);
|
|
1593
|
+
void this.enqueueAndDispatchDelivery(chatId, 'insertion_status', `insertion:${item.insertionId}:ordinary`, 'ℹ️ 当前会话已确认可接收新轮次,且这条消息确定未被插入;现已交回普通队列,只执行一次。', [], item.messageId, `insertion:${item.insertionId}:ordinary`).catch(() => { });
|
|
1594
|
+
void this.processQueue(chatId, true).catch(() => { });
|
|
1467
1595
|
}
|
|
1468
1596
|
else if (state === 'accepted') {
|
|
1597
|
+
this.store.deletePendingInsertionNotices(this.config.name, chatId, item.insertionId);
|
|
1469
1598
|
this.store.clearPendingTrigger(this.config.name, chatId, item.rowId);
|
|
1470
1599
|
// Durable acceptance drives Get, including after bridge restart when
|
|
1471
1600
|
// its ephemeral pendingAckMessages array no longer exists.
|
|
@@ -1486,8 +1615,15 @@ export class FeishuBot {
|
|
|
1486
1615
|
if (item.state !== state)
|
|
1487
1616
|
void this.enqueueAndDispatchDelivery(chatId, 'insertion_status', `insertion:${item.insertionId}:${state}`, `ℹ️ 这条插入消息的状态为 ${state},仍保留本地记录,未确认接收前不会显示 Get,也不会自动重复执行。${receipt.reason || ''}`, [], item.messageId, `insertion:${item.insertionId}:${state}`).catch(() => { });
|
|
1488
1617
|
}
|
|
1489
|
-
else if (state === 'unknown' &&
|
|
1490
|
-
|
|
1618
|
+
else if (state === 'unknown' && Date.now() - record.unknownSince >= 45_000 && !item.unknownNotified) {
|
|
1619
|
+
const text = 'ℹ️ 这条插入消息约 45 秒仍未确认接收:本地记录保留中,不会自动重复执行;确认后会自动显示 Get。';
|
|
1620
|
+
const key = `insertion:${item.insertionId}:unknown-prolonged`;
|
|
1621
|
+
if (this.store.queueInsertionUnknownNotice(this.config.name, chatId, item.rowId, {
|
|
1622
|
+
sessionKey: this.getSessionKey(chatId), chatId, botName: this.config.name, sourceType: 'insertion_status', sourceId: key, deliveryKey: key,
|
|
1623
|
+
contentHash: this.stableHash(`${text}|[]`), content: text, attachmentsJson: '[]', replyToMessageId: item.messageId,
|
|
1624
|
+
deliveryMetaJson: JSON.stringify({ botIdentity: this.config.appId, insertionNotice: { rowId: item.rowId, insertionId: item.insertionId, sessionId: item.sessionId, stopEpoch: item.stopEpoch } }),
|
|
1625
|
+
}))
|
|
1626
|
+
void this.dispatchPendingDeliveries(chatId).catch(() => { });
|
|
1491
1627
|
}
|
|
1492
1628
|
}
|
|
1493
1629
|
}
|
|
@@ -1614,12 +1750,46 @@ export class FeishuBot {
|
|
|
1614
1750
|
catch { /* observer failures are isolated */ }
|
|
1615
1751
|
}
|
|
1616
1752
|
}
|
|
1617
|
-
async processQueue(chatId) {
|
|
1753
|
+
async processQueue(chatId, activityChecked = false) {
|
|
1618
1754
|
const existing = this.queueRuns.get(chatId);
|
|
1619
1755
|
if (existing) {
|
|
1620
1756
|
console.log(`[${this.config.name}] processQueue already running for ${chatId.slice(-8)}, joining existing run`);
|
|
1621
1757
|
return existing;
|
|
1622
1758
|
}
|
|
1759
|
+
const detached = this.store.getDispatchCustody(this.config.name, chatId);
|
|
1760
|
+
if (detached) {
|
|
1761
|
+
// Startup/recovery can reach the queue without an ingress handler. Pin
|
|
1762
|
+
// unsent triggers synchronously before consulting live host activity.
|
|
1763
|
+
for (const message of this.store.getPendingTriggerMessages(this.config.name, chatId)) {
|
|
1764
|
+
if (!message.id || message.senderType !== 'human' || this.store.getInsertion(this.config.name, chatId, message.id))
|
|
1765
|
+
continue;
|
|
1766
|
+
this.store.saveInsertion(this.config.name, chatId, message.id, JSON.stringify([this.config.appId, chatId, message.messageId]), 'pending', {
|
|
1767
|
+
messageId: message.messageId, text: message.content, stopEpoch: this.stopGeneration(chatId), expectedRunId: detached.runId,
|
|
1768
|
+
ownerId: detached.id, batchId: detached.batchId, sessionId: detached.sessionId, sessionKey: this.getSessionKey(chatId),
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
if (await this.reconcileDispatchCustody(chatId))
|
|
1772
|
+
return this.processQueue(chatId, true);
|
|
1773
|
+
void this.reconcileInsertions(chatId).catch(() => { });
|
|
1774
|
+
this.scheduleInsertionRecovery();
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (!activityChecked && typeof this.openclawClient.getSessionRuntimeStatus === 'function' && typeof this.openclawClient.getSessionDispatchStatus === 'function') {
|
|
1778
|
+
const candidates = this.store.getPendingTriggerMessages(this.config.name, chatId)
|
|
1779
|
+
.filter(message => message.id && message.senderType === 'human' && !this.store.isInsertionReserved(this.config.name, chatId, message.id));
|
|
1780
|
+
if (!candidates.length)
|
|
1781
|
+
return;
|
|
1782
|
+
for (const message of candidates)
|
|
1783
|
+
await this.reconcileCurrentInputActivity(chatId, message.id, message.messageId, message.content);
|
|
1784
|
+
// Re-enter through the ordinary owner CAS after all awaits. Any concurrent
|
|
1785
|
+
// admission now owns its batch; never submit in parallel with that owner.
|
|
1786
|
+
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));
|
|
1787
|
+
if (eligible)
|
|
1788
|
+
return this.processQueue(chatId, true);
|
|
1789
|
+
void this.reconcileInsertions(chatId).catch(() => { });
|
|
1790
|
+
this.scheduleInsertionRecovery();
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1623
1793
|
const owner = { id: randomUUID(), token: {}, revoked: false, stopEpoch: this.stopGeneration(chatId) };
|
|
1624
1794
|
this.queueOwners.set(chatId, owner);
|
|
1625
1795
|
const run = this.processQueueInner(chatId, owner).finally(() => {
|
|
@@ -1645,13 +1815,16 @@ export class FeishuBot {
|
|
|
1645
1815
|
* the proactive path are invisible to them. Steer-fallback triggers are
|
|
1646
1816
|
* restored via the failure observers so their input drains normally.
|
|
1647
1817
|
*/
|
|
1648
|
-
forceExpireQueueOwner(chatId, reason) {
|
|
1818
|
+
forceExpireQueueOwner(chatId, reason, preserveDispatch = false) {
|
|
1649
1819
|
const owner = this.queueRuns.get(chatId);
|
|
1650
1820
|
if (!owner)
|
|
1651
1821
|
return false;
|
|
1652
1822
|
const context = this.queueOwners.get(chatId);
|
|
1823
|
+
if (context?.batchId && !preserveDispatch)
|
|
1824
|
+
this.store.clearDispatchCustody(this.config.name, chatId, context.id, context.batchId);
|
|
1653
1825
|
if (context)
|
|
1654
1826
|
context.revoked = true;
|
|
1827
|
+
this.busyChats.set(chatId, 0);
|
|
1655
1828
|
console.warn(`[${this.config.name}] force-expiring zombie queue owner for ${chatId.slice(-8)}: ${reason}`);
|
|
1656
1829
|
// Fence the stale batch's local observation BEFORE releasing custody: an
|
|
1657
1830
|
// alive old poller would keep destructively consuming the replacement
|
|
@@ -1679,6 +1852,10 @@ export class FeishuBot {
|
|
|
1679
1852
|
owner.batchId = randomUUID();
|
|
1680
1853
|
owner.triggerIds = undefined;
|
|
1681
1854
|
owner.runId = undefined;
|
|
1855
|
+
owner.admitted = false;
|
|
1856
|
+
owner.collecting = false;
|
|
1857
|
+
owner.yielded = false;
|
|
1858
|
+
owner.sessionId = undefined;
|
|
1682
1859
|
this.queueRunTokens.set(chatId, batchToken);
|
|
1683
1860
|
// A fresh batch starts: steered inputs admitted into the PREVIOUS batch
|
|
1684
1861
|
// completed with it (consumed or not); their failure observers are stale.
|
|
@@ -1836,6 +2013,9 @@ export class FeishuBot {
|
|
|
1836
2013
|
try {
|
|
1837
2014
|
sessionKey = await this.ensureSession(chatId);
|
|
1838
2015
|
guard();
|
|
2016
|
+
const generation = await this.openclawClient.getSessionInfo(sessionKey, 1500).catch(() => null);
|
|
2017
|
+
guard();
|
|
2018
|
+
owner.sessionId = generation?.session?.sessionId;
|
|
1839
2019
|
}
|
|
1840
2020
|
catch (err) {
|
|
1841
2021
|
if (!current())
|
|
@@ -1923,11 +2103,14 @@ export class FeishuBot {
|
|
|
1923
2103
|
// OpenClaw accepts it, retrying would duplicate the same message.
|
|
1924
2104
|
markSubmittedBatch("send-attempt");
|
|
1925
2105
|
},
|
|
1926
|
-
onRunIdentified: (runId) => { guard(); owner.runId = runId; foregroundRunId = runId; },
|
|
2106
|
+
onRunIdentified: (runId, identity) => { guard(); owner.runId = runId; owner.sessionId = identity?.sessionId || owner.sessionId; foregroundRunId = runId; },
|
|
1927
2107
|
onSubmitted: async (runId) => {
|
|
1928
2108
|
guard();
|
|
1929
2109
|
owner.runId = runId;
|
|
2110
|
+
owner.admitted = true;
|
|
2111
|
+
this.store.saveDispatchCustody(this.config.name, chatId, { id: owner.id, batchId: owner.batchId, runId, sessionId: owner.sessionId, admitted: true, stopEpoch: owner.stopEpoch, sessionKey });
|
|
1930
2112
|
void this.reconcileInsertions(chatId).catch(() => { });
|
|
2113
|
+
this.bindRunDeliveryTarget(chatId, runId, { triggerId, messageId: lastHuman.messageId || '', liveStatus });
|
|
1931
2114
|
for (const ack of this.pendingAckMessages.get(chatId) || []) {
|
|
1932
2115
|
if (!mergedTriggerIdSet.has(ack.rowId))
|
|
1933
2116
|
continue;
|
|
@@ -1950,6 +2133,21 @@ export class FeishuBot {
|
|
|
1950
2133
|
markSubmittedBatch(`submitted:${runId}`);
|
|
1951
2134
|
},
|
|
1952
2135
|
onProgress: (event) => current() ? liveStatus?.progress(event) : undefined,
|
|
2136
|
+
onCollectionStarted: runId => { guard(); if (owner.runId === runId)
|
|
2137
|
+
owner.collecting = true; },
|
|
2138
|
+
onYieldHandoff: runId => {
|
|
2139
|
+
guard();
|
|
2140
|
+
if (owner.runId !== runId || !owner.batchId)
|
|
2141
|
+
throw new Error('Yield root/batch identity mismatch');
|
|
2142
|
+
// All durable result routing commits synchronously before the
|
|
2143
|
+
// collector is fenced and before another input may take custody.
|
|
2144
|
+
this.bindRunDeliveryTarget(chatId, runId, { triggerId, messageId: lastHuman.messageId || '', liveStatus });
|
|
2145
|
+
owner.yielded = true;
|
|
2146
|
+
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 });
|
|
2147
|
+
this.forceExpireQueueOwner(chatId, 'trusted exact-root yield; background result custody retained', true);
|
|
2148
|
+
void this.reconcileInsertions(chatId).catch(() => { });
|
|
2149
|
+
this.scheduleInsertionRecovery();
|
|
2150
|
+
},
|
|
1953
2151
|
onWaitPaused: async (notice) => {
|
|
1954
2152
|
guard();
|
|
1955
2153
|
await liveStatus?.showWaitingForResult(notice.message);
|
|
@@ -2189,6 +2387,10 @@ export class FeishuBot {
|
|
|
2189
2387
|
}
|
|
2190
2388
|
catch (err) {
|
|
2191
2389
|
if (!current() || err instanceof SupersededObservation) {
|
|
2390
|
+
if (current()) {
|
|
2391
|
+
this.busyChats.set(chatId, 0);
|
|
2392
|
+
owner.revoked = true;
|
|
2393
|
+
}
|
|
2192
2394
|
// This batch's local observation was fenced by a newer batch (or a
|
|
2193
2395
|
// custody revocation). The new owner handles delivery from here; end
|
|
2194
2396
|
// quietly — no error notice, no replay, no Gateway abort. The old
|
|
@@ -2295,8 +2497,11 @@ export class FeishuBot {
|
|
|
2295
2497
|
break;
|
|
2296
2498
|
}
|
|
2297
2499
|
finally {
|
|
2298
|
-
if (current())
|
|
2500
|
+
if (current()) {
|
|
2299
2501
|
this.busyChats.set(chatId, 0);
|
|
2502
|
+
if (owner.batchId && !owner.yielded)
|
|
2503
|
+
this.store.clearDispatchCustody(this.config.name, chatId, owner.id, owner.batchId);
|
|
2504
|
+
}
|
|
2300
2505
|
}
|
|
2301
2506
|
// Check if more messages arrived while we were busy
|
|
2302
2507
|
// Small delay to let any in-flight messages settle
|
|
@@ -2793,7 +2998,7 @@ export class FeishuBot {
|
|
|
2793
2998
|
const previous = this.deliveryTargetsByRun.get(key);
|
|
2794
2999
|
if (previous?.timer)
|
|
2795
3000
|
clearTimeout(previous.timer);
|
|
2796
|
-
this.store.bindDeliveryRun(this.config.name, chatId, runId, { triggerId: target.triggerId, messageId: target.messageId, runId, sessionKey: `agent:main:${this.getSessionKey(chatId).replace(/^agent:[^:]+:/, '')}` });
|
|
3001
|
+
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:[^:]+:/, '')}` });
|
|
2797
3002
|
this.deliveryTargetsByRun.set(key, {
|
|
2798
3003
|
triggerId: target.triggerId,
|
|
2799
3004
|
messageId: target.messageId,
|
|
@@ -3152,7 +3357,7 @@ export class FeishuBot {
|
|
|
3152
3357
|
this.store.saveErrorNotice(record);
|
|
3153
3358
|
if (verdict.state === "terminal" && verdict.terminalKey && verdict.text) {
|
|
3154
3359
|
// Independent identity: never take the result's trigger/final key.
|
|
3155
|
-
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);
|
|
3360
|
+
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);
|
|
3156
3361
|
// The send-time recheck may have parked or suppressed the observation.
|
|
3157
3362
|
const refreshed = this.store.getErrorNotice(this.config.name, chatId, record.noticeId);
|
|
3158
3363
|
if (refreshed?.verdict && ["pending", "suppressed", "stopped"].includes(refreshed.verdict.state))
|
|
@@ -3202,6 +3407,14 @@ export class FeishuBot {
|
|
|
3202
3407
|
if (this.store.getDeliveryByKey(this.config.name, chatId, source)?.status !== "delivered")
|
|
3203
3408
|
throw new Error("Go executor outbox delivery pending/unconfirmed");
|
|
3204
3409
|
}
|
|
3410
|
+
insertionNoticeCurrent(chatId, notice) {
|
|
3411
|
+
if (!notice)
|
|
3412
|
+
return true;
|
|
3413
|
+
const current = this.store.getInsertion(this.config.name, chatId, notice.rowId);
|
|
3414
|
+
return current?.insertionId === notice.insertionId && current.state === 'unknown' && current.stopEpoch === notice.stopEpoch &&
|
|
3415
|
+
this.stopGeneration(chatId) === notice.stopEpoch && current.sessionId === notice.sessionId &&
|
|
3416
|
+
Number.isFinite(current.unknownSince) && Date.now() - current.unknownSince >= 45_000;
|
|
3417
|
+
}
|
|
3205
3418
|
async dispatchPendingDeliveries(chatId, replyToMessageId) {
|
|
3206
3419
|
this.installGoReceiptCapture();
|
|
3207
3420
|
for (const id of this.localDeliveryRetryCustody.get(chatId) || []) {
|
|
@@ -3220,6 +3433,20 @@ export class FeishuBot {
|
|
|
3220
3433
|
if (pending.length === 0)
|
|
3221
3434
|
break;
|
|
3222
3435
|
for (const item of pending) {
|
|
3436
|
+
const notice = JSON.parse(item.deliveryMetaJson || '{}').insertionNotice;
|
|
3437
|
+
if (notice) {
|
|
3438
|
+
const info = await this.openclawClient.getSessionInfo(this.getSessionKey(chatId), 1500).catch(() => null);
|
|
3439
|
+
if (!this.insertionNoticeCurrent(chatId, notice) || info?.session?.archived === true ||
|
|
3440
|
+
info?.session?.sessionId && info.session.sessionId !== notice.sessionId) {
|
|
3441
|
+
this.store.markDeliveryFailed(item.id);
|
|
3442
|
+
continue;
|
|
3443
|
+
}
|
|
3444
|
+
if (!info?.session?.sessionId) {
|
|
3445
|
+
this.scheduleDeliveryRetry(chatId, 1);
|
|
3446
|
+
retryScheduled = true;
|
|
3447
|
+
continue;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3223
3450
|
const ownerId = JSON.parse(item.deliveryMetaJson || "{}").provenance?.goJobId;
|
|
3224
3451
|
const deliveryJob = ownerId ? this.store.getGoJobById(ownerId) : this.store.getActiveGoJob(this.config.name, chatId);
|
|
3225
3452
|
const receipts = [
|
|
@@ -3228,6 +3455,8 @@ export class FeishuBot {
|
|
|
3228
3455
|
];
|
|
3229
3456
|
await this.sendOrdered(chatId, () => this.goDeliveryReceipts.run({ receipts, goId: deliveryJob?.id, outboxId: item.id, chatId, stage: "body", ordinals: new Map(), effectGuard: () => {
|
|
3230
3457
|
const meta = JSON.parse(item.deliveryMetaJson || '{}'), p = meta.provenance;
|
|
3458
|
+
if (!this.insertionNoticeCurrent(chatId, meta.insertionNotice))
|
|
3459
|
+
throw Object.assign(new Error('Insertion notice no longer current'), { deliveryStopped: true });
|
|
3231
3460
|
if ((p?.final || item.sourceType === 'run_error') &&
|
|
3232
3461
|
(typeof meta.deliveryStopEpoch === 'number' && meta.deliveryStopEpoch !== this.stopGeneration(chatId) ||
|
|
3233
3462
|
p?.sessionKey && p.runId && this.store.isStoppedObservation(p.sessionKey, p.runId))) {
|
|
@@ -3370,6 +3599,12 @@ export class FeishuBot {
|
|
|
3370
3599
|
}
|
|
3371
3600
|
}
|
|
3372
3601
|
this.store.markDeliveryDelivered(item.id);
|
|
3602
|
+
if (item.sourceType === 'run_error' && provenance?.terminalEvidence?.state === 'terminal' && provenance.runId && !provenance.runId.startsWith('announce:')) {
|
|
3603
|
+
this.store.settleRunReactionIntents(this.config.name, chatId, provenance.runId);
|
|
3604
|
+
if (provenance.sessionKey)
|
|
3605
|
+
this.store.finishRunObservation(provenance.sessionKey, provenance.runId);
|
|
3606
|
+
void this.reconcileReactions(chatId).catch(() => { });
|
|
3607
|
+
}
|
|
3373
3608
|
if (provenance?.final && provenance.runId) {
|
|
3374
3609
|
this.store.settleRunReactionIntents(this.config.name, chatId, provenance.runId);
|
|
3375
3610
|
void this.reconcileReactions(chatId).catch(() => { });
|
|
@@ -4877,7 +5112,10 @@ export class FeishuBot {
|
|
|
4877
5112
|
this.store.stopInsertions(this.config.name, chatId);
|
|
4878
5113
|
this.store.cancelReactionIntents(this.config.name, chatId, this.stopGeneration(chatId));
|
|
4879
5114
|
void this.openclawClient.cancelSteerSession?.(sessionKey, this.stopGeneration(chatId)).catch(() => { });
|
|
4880
|
-
|
|
5115
|
+
this.forceExpireQueueOwner(chatId, 'explicit session reset');
|
|
5116
|
+
this.store.clearAllPendingTriggers(this.config.name, chatId);
|
|
5117
|
+
// Fire reset (no response expected). Client fences old exact observations
|
|
5118
|
+
// and exposes its reset barrier to new post-cutoff human inputs.
|
|
4881
5119
|
this.openclawClient.resetSession(sessionKey).catch(() => { });
|
|
4882
5120
|
// Wait a moment for it to take effect
|
|
4883
5121
|
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,16 +132,25 @@ 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;
|
|
131
140
|
finishRunObservation(sessionKey: string, runId: string, state?: string): void;
|
|
132
141
|
listRunObservations(sessionKey?: string, due?: number, limit?: number): any[];
|
|
133
142
|
saveInsertion(bot: string, chat: string, row: number, id: string, state: string, record: object, next?: number): void;
|
|
143
|
+
/** Purge still-pending insertion status notices once the insertion resolves
|
|
144
|
+
* (accepted/ordinary): a stale "unknown" notice must not fire after Get. */
|
|
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;
|
|
134
149
|
claimInsertionAttempt(bot: string, chat: string, row: number): boolean;
|
|
135
150
|
getInsertion(bot: string, chat: string, row: number): any;
|
|
136
151
|
listInsertions(bot: string, chat?: string): any[];
|
|
137
152
|
isInsertionReserved(bot: string, chat: string, row: number): boolean;
|
|
153
|
+
stopInsertionAtEpoch(bot: string, chat: string, row: number, epoch: number): void;
|
|
138
154
|
stopInsertions(bot: string, chat: string): void;
|
|
139
155
|
hasUnknownDeliveryOperation(outbox: number): boolean;
|
|
140
156
|
/** Retain the full known result BEFORE the fallible main-journal checkpoint.
|