openclaw-lark-multi-agent 1.8.10 → 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.
@@ -1319,11 +1319,20 @@ export class FeishuBot {
1319
1319
  return;
1320
1320
  // Session creation/model setup is not agent admission. Initialize a truly
1321
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.
1322
+ // missing describe response. A failed setup is not dispatch-idle evidence;
1323
+ // retain this never-attempted input for paced reconciliation as well.
1323
1324
  try {
1324
1325
  await this.ensureSession(chatId);
1325
1326
  }
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
+ }
1327
1336
  return;
1328
1337
  }
1329
1338
  if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
@@ -1344,8 +1353,8 @@ export class FeishuBot {
1344
1353
  if (current ? fresh?.insertionId !== current.insertionId || fresh.state !== 'pending' || fresh.attempted === true || !fresh.activityUnbound || !!fresh.expectedRunId : !!fresh)
1345
1354
  return;
1346
1355
  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);
1356
+ const idle = compatible && !status.running && !status.archived && !activity.archived && !activity.busyConfirmed &&
1357
+ activity.idleConfirmed === true && !!activity.sessionId;
1349
1358
  if (idle) {
1350
1359
  if (current)
1351
1360
  this.store.saveInsertion(this.config.name, chatId, rowId, current.insertionId, 'ordinary', { ...current, activityUnbound: false });
@@ -1374,11 +1383,9 @@ export class FeishuBot {
1374
1383
  return this.stopGeneration(chatId) === epoch && now?.id === saved.id && now?.batchId === saved.batchId && now?.runId === saved.runId &&
1375
1384
  this.queueOwners.get(chatId) === local && (!local || local.batch === batch && local.runId === runId && (local.admitted === true || local.yielded === true) && local.collecting === true);
1376
1385
  };
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
- }
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;
1382
1389
  const status = await this.openclawClient.getSessionRuntimeStatus(this.getSessionKey(chatId)).catch(() => null);
1383
1390
  if (!unchanged() || !status?.idleConfirmed || status.archived || status.sessionId !== saved.sessionId)
1384
1391
  return false;
@@ -1556,18 +1563,12 @@ export class FeishuBot {
1556
1563
  const status = await this.openclawClient.getSessionRuntimeStatus(item.sessionKey).catch(() => null);
1557
1564
  if (!stillCurrent())
1558
1565
  continue;
1559
- const rootSettled = await this.openclawClient.exactRunSettled?.(item.expectedRunId);
1560
- if (!stillCurrent())
1561
- continue;
1562
- const activity = !owner && !rootSettled && status?.idleConfirmed ? await this.openclawClient.getSessionDispatchStatus?.(item.sessionKey).catch(() => null) : null;
1566
+ const activity = status?.idleConfirmed ? await this.openclawClient.getSessionDispatchStatus?.(item.sessionKey).catch(() => null) : null;
1563
1567
  if (!stillCurrent())
1564
1568
  continue;
1565
- const currentIdle = activity?.idleConfirmed === true && activity.sessionId === item.sessionId && !activity.archived && status?.sessionId === item.sessionId;
1569
+ const currentIdle = activity?.idleConfirmed === true && !activity.busyConfirmed && activity.sessionId === item.sessionId && !activity.archived && status?.sessionId === item.sessionId;
1566
1570
  const unchanged = this.queueOwners.get(chatId) === owner && owner?.batch === batchAtCheck && owner?.runId === runAtCheck;
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) {
1571
+ if (currentIdle && status?.idleConfirmed === true && unchanged) {
1571
1572
  state = 'ordinary';
1572
1573
  // Never expire a successor merely to schedule the old input.
1573
1574
  if (runAtCheck === item.expectedRunId)
@@ -71,6 +71,7 @@ export declare class OpenClawClient {
71
71
  /** Deduplicates 2026.8 canonical `agent.stream=tool` events against legacy
72
72
  * `stream=item` mirrors when a runtime emits both representations. */
73
73
  private recentToolEventKeys;
74
+ private privateInputToolCalls;
74
75
  private sessionSettlementCallbacks;
75
76
  private sessionMessageCallbacks;
76
77
  private progressCallbacks;
@@ -138,9 +139,18 @@ export declare class OpenClawClient {
138
139
  connect(): Promise<void>;
139
140
  private _doConnect;
140
141
  private extractVisibleUserText;
142
+ /** Role is presentation, not authorship. Core's public forwarded-message
143
+ * projection preserves these input markers even when it changes user to
144
+ * assistant. Stream events without a message/role remain supported; never
145
+ * inspect run-level inputProvenance (a real reply may answer such an input). */
146
+ private isInputRecord;
141
147
  private extractVisibleAssistantText;
142
148
  private isVerboseTranscriptEnabled;
143
149
  private clearVerboseAssistantState;
150
+ /** Internal message tools carry private input, not assistant narration.
151
+ * Redact before buffering as well as progress callbacks (including failure
152
+ * summaries). Preserve tool identity/phase for activity and acceptance logic. */
153
+ private redactPrivateInputTool;
144
154
  /** Render protocol-4 structured tool args/results without `[object Object]`.
145
155
  * Prefer useful, non-secret fields and keep the fallback bounded. */
146
156
  private formatToolValue;
@@ -82,6 +82,7 @@ export class OpenClawClient {
82
82
  /** Deduplicates 2026.8 canonical `agent.stream=tool` events against legacy
83
83
  * `stream=item` mirrors when a runtime emits both representations. */
84
84
  recentToolEventKeys = new Map();
85
+ privateInputToolCalls = new Set();
85
86
  sessionSettlementCallbacks = new Map();
86
87
  sessionMessageCallbacks = new Map();
87
88
  progressCallbacks = new Map();
@@ -344,6 +345,16 @@ export class OpenClawClient {
344
345
  p.reject(new Error(`RPC error: ${JSON.stringify(frame.error)}`));
345
346
  }
346
347
  }
348
+ if (frame.event === 'agent' && ['tool', 'item'].includes(frame.payload?.stream))
349
+ this.redactPrivateInputTool(frame.payload);
350
+ // Public transcript projections can render an INPUT as role=assistant
351
+ // (forwarded sessions_send / internal receipts). Reject its structured
352
+ // identity before any collector, proactive callback or progress buffer.
353
+ // Do not classify by text, run ownership, or a wrapping prompt prefix:
354
+ // genuine replies/quotes and external Cron/child outputs remain valid.
355
+ if ((frame.event === 'chat' ||
356
+ frame.event === 'agent' && ['assistant', 'chatDelta'].includes(frame.payload?.stream)) && this.isInputRecord(frame.payload))
357
+ return;
347
358
  // Agent events — store per session key
348
359
  if (frame.event === "agent" || frame.event === "chat") {
349
360
  const sk = frame.payload?.sessionKey || "__default__";
@@ -412,7 +423,8 @@ export class OpenClawClient {
412
423
  if (bucket.length > 2048)
413
424
  bucket.splice(0, bucket.length - 2048);
414
425
  }
415
- if (frame.type === "event" && ["agent", "chat", "session.message"].includes(frame.event)) {
426
+ if (frame.type === "event" && ["agent", "chat", "session.message"].includes(frame.event) &&
427
+ !(frame.event === 'session.message' && this.isInputRecord(frame.payload))) {
416
428
  for (const observer of this.goEvidenceObservers) {
417
429
  try {
418
430
  observer({ channel: frame.event, payload: frame.payload });
@@ -448,7 +460,8 @@ export class OpenClawClient {
448
460
  // in correct time order and flip the reaction to Get.
449
461
  this.handleSteerConsumption(rawKey, visibleUserText);
450
462
  }
451
- const visibleAssistantText = this.extractVisibleAssistantText(msg);
463
+ const inputRecord = this.isInputRecord(frame.payload);
464
+ const visibleAssistantText = inputRecord ? "" : this.extractVisibleAssistantText(msg);
452
465
  if (visibleAssistantText) {
453
466
  if (!this.agentEvents.has(rawKey))
454
467
  this.agentEvents.set(rawKey, []);
@@ -459,7 +472,8 @@ export class OpenClawClient {
459
472
  data: { deltaText: visibleAssistantText, delta: visibleAssistantText, replace: true },
460
473
  });
461
474
  }
462
- this.handleProactiveSessionMessage(rawKey, msg, frame.payload.runId, frame.payload.sessionId);
475
+ if (!inputRecord)
476
+ this.handleProactiveSessionMessage(rawKey, msg, frame.payload.runId, frame.payload.sessionId);
463
477
  for (const wake of this.eventCollectors)
464
478
  wake();
465
479
  // Tool calls in assistant messages — skip, using agent item events instead
@@ -598,8 +612,26 @@ export class OpenClawClient {
598
612
  .join("\n")
599
613
  .trim();
600
614
  }
615
+ /** Role is presentation, not authorship. Core's public forwarded-message
616
+ * projection preserves these input markers even when it changes user to
617
+ * assistant. Stream events without a message/role remain supported; never
618
+ * inspect run-level inputProvenance (a real reply may answer such an input). */
619
+ isInputRecord(record) {
620
+ if (!record || typeof record !== 'object')
621
+ return false;
622
+ if (typeof record.role === 'string' && record.role !== 'assistant')
623
+ return true;
624
+ if (['external_user', 'inter_session', 'internal_system'].includes(record.provenance?.kind))
625
+ return true;
626
+ if (record.senderSession && typeof record.senderSession === 'object')
627
+ return true;
628
+ const inputKey = record.idempotencyKey ?? record.__openclaw?.idempotencyKey;
629
+ if (typeof inputKey === 'string' && inputKey.endsWith(':user'))
630
+ return true;
631
+ return this.isInputRecord(record.message) || this.isInputRecord(record.data);
632
+ }
601
633
  extractVisibleAssistantText(msg, options = {}) {
602
- if (msg?.role !== "assistant")
634
+ if (msg?.role !== "assistant" || this.isInputRecord(msg))
603
635
  return "";
604
636
  const content = msg.content;
605
637
  // Extract only visible text parts and ignore thinking/tool blocks.
@@ -638,13 +670,48 @@ export class OpenClawClient {
638
670
  this.verboseAssistantLastTouched.delete(key);
639
671
  }
640
672
  }
673
+ /** Internal message tools carry private input, not assistant narration.
674
+ * Redact before buffering as well as progress callbacks (including failure
675
+ * summaries). Preserve tool identity/phase for activity and acceptance logic. */
676
+ redactPrivateInputTool(payload) {
677
+ const data = payload?.data;
678
+ if (!data || payload.stream === 'item' && data.kind !== 'tool')
679
+ return;
680
+ const name = String(data.name || data.toolName || '').split('.').pop();
681
+ const eventId = String(data.toolCallId || data.itemId || data.id || '').replace(/^(?:tool|command):/, '');
682
+ const key = JSON.stringify([this.canonicalSessionKey(payload.sessionKey || ''), payload.runId || '', eventId]);
683
+ let args = data.input ?? data.args ?? data.meta;
684
+ if (typeof args === 'string') {
685
+ try {
686
+ args = JSON.parse(args);
687
+ }
688
+ catch {
689
+ args = undefined;
690
+ }
691
+ }
692
+ const wrapped = name === 'tool_call' && /(?:^|[.:])sessions_(?:send|yield)$/.test(String(args?.id || ''));
693
+ const privateBody = name === 'sessions_send' || name === 'sessions_yield' || wrapped;
694
+ if (privateBody && eventId) {
695
+ this.privateInputToolCalls.add(key);
696
+ if (this.privateInputToolCalls.size > 2048)
697
+ this.privateInputToolCalls.delete(this.privateInputToolCalls.values().next().value);
698
+ }
699
+ if (!privateBody && !this.privateInputToolCalls.has(key))
700
+ return;
701
+ payload.data = Object.fromEntries(['kind', 'phase', 'name', 'toolName', 'toolCallId', 'itemId', 'id', 'isError'].filter(k => data[k] !== undefined).map(k => [k, data[k]]));
702
+ }
641
703
  /** Render protocol-4 structured tool args/results without `[object Object]`.
642
704
  * Prefer useful, non-secret fields and keep the fallback bounded. */
643
705
  formatToolValue(value, toolName, phase) {
644
706
  // sessions_yield.message is private resumed context. Do not mirror its
645
707
  // arguments/results (including JSON strings) into process cards or verbose
646
708
  // tool replies. This is redaction only, never evidence of a handoff.
647
- if (toolName === "sessions_yield")
709
+ if (toolName === "sessions_yield" || toolName === "sessions_send")
710
+ return "";
711
+ // Generic public-tool wrapper: redact only the internal send operation,
712
+ // not all tool bodies or outputs. Its returned body can echo the input.
713
+ if (toolName === 'tool_call' && value && typeof value === 'object' &&
714
+ /(?:^|[.:])sessions_send$/.test(String(value.id || '')))
648
715
  return "";
649
716
  if (value === null || value === undefined || value === "")
650
717
  return "";
@@ -779,6 +846,8 @@ export class OpenClawClient {
779
846
  return true;
780
847
  }
781
848
  handleVerboseAssistantStream(payload) {
849
+ if (this.isInputRecord(payload))
850
+ return;
782
851
  const rawKey = payload?.sessionKey || "";
783
852
  if (!rawKey)
784
853
  return;
@@ -806,6 +875,8 @@ export class OpenClawClient {
806
875
  }
807
876
  }
808
877
  handleProactiveSessionMessage(rawKey, msg, runId, sessionId) {
878
+ if (this.isInputRecord(msg))
879
+ return false;
809
880
  const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
810
881
  // Proactive assistant text messages. Cron/session-targeted runs often emit
811
882
  // structured content arrays rather than a plain string. Extract only visible
@@ -2123,16 +2194,93 @@ export class OpenClawClient {
2123
2194
  * terminal caches. Unknown/missing fields never authorize scheduling. */
2124
2195
  async getSessionDispatchStatus(sessionKey) {
2125
2196
  let timer;
2197
+ const key = this.canonicalSessionKey(sessionKey), deadline = Date.now() + 1500;
2198
+ // One budget covers the entire public pending scan, not 1500ms per page.
2199
+ // Retained interrupted/cancelled inputs are recovery records, NOT runnable
2200
+ // queue entries. Never delete, mutate or replay them to prove schedulability.
2201
+ const scan = async () => {
2202
+ let sessionId, total, before;
2203
+ const seen = new Set(), cursors = new Set();
2204
+ let headSignature = '';
2205
+ const unknown = () => ({ idleConfirmed: false, sessionId });
2206
+ const validId = (value) => value === undefined || typeof value === 'string' && !!value.trim();
2207
+ for (let page = 0; page < 32; page++) {
2208
+ const history = await this.rpc('chat.history', { sessionKey: key, limit: 20, ...(before ? { pendingBefore: before } : {}) }, 1500);
2209
+ if (Date.now() >= deadline)
2210
+ return unknown();
2211
+ const info = history?.sessionInfo;
2212
+ const id = info?.sessionId || history?.sessionId;
2213
+ if (!validId(info?.sessionId) || !validId(history?.sessionId) || history?.sessionKey !== key || typeof id !== 'string' || !id.trim() ||
2214
+ info?.sessionId && history?.sessionId && info.sessionId !== history.sessionId || sessionId && sessionId !== id)
2215
+ return unknown();
2216
+ sessionId = id;
2217
+ const base = { sessionId, archived: info?.archived === true };
2218
+ if (base.archived)
2219
+ return { ...base, idleConfirmed: false };
2220
+ if (info?.hasActiveRun === true || history?.inFlightRun)
2221
+ return { ...base, idleConfirmed: false, busyConfirmed: true,
2222
+ activeRunId: typeof history?.inFlightRun?.runId === 'string' ? history.inFlightRun.runId : undefined };
2223
+ const pending = history?.pendingInputs;
2224
+ if (!Number.isSafeInteger(pending?.total) || pending.total < 0 || total !== undefined && total !== pending.total)
2225
+ return unknown();
2226
+ total = pending.total;
2227
+ if (pending.queuedCount !== undefined && (!Number.isSafeInteger(pending.queuedCount) || pending.queuedCount < 0))
2228
+ return unknown();
2229
+ if (pending.queuedCount > 0)
2230
+ return { ...base, idleConfirmed: false, busyConfirmed: true };
2231
+ // Legacy empty pending responses omit items. Only total=0 can safely
2232
+ // support that shape; a hidden/nonempty retained set is never guessed.
2233
+ const items = pending.items === undefined && total === 0 ? [] : pending.items;
2234
+ if (!Array.isArray(items))
2235
+ return unknown();
2236
+ for (const item of items) {
2237
+ if (typeof item?.id !== 'string' || !item.id || !['queued', 'cancelled', 'interrupted'].includes(item.state) || seen.has(item.id))
2238
+ return unknown();
2239
+ seen.add(item.id);
2240
+ if (item.state === 'queued')
2241
+ return { ...base, idleConfirmed: false, busyConfirmed: true };
2242
+ }
2243
+ if (seen.size > total)
2244
+ return unknown();
2245
+ const next = pending.nextBefore;
2246
+ if (next !== undefined && (!Number.isSafeInteger(next) || next <= 0 || cursors.has(next) || before !== undefined && next >= before))
2247
+ return unknown();
2248
+ const signature = JSON.stringify([id, total, info?.hasActiveRun, items.map((item) => [item.id, item.state]), next]);
2249
+ if (page === 0)
2250
+ headSignature = signature;
2251
+ if (next !== undefined) {
2252
+ cursors.add(next);
2253
+ before = next;
2254
+ continue;
2255
+ }
2256
+ if (info?.hasActiveRun !== false || seen.size !== total)
2257
+ return unknown();
2258
+ if (page > 0) {
2259
+ // Pagination is not an atomic snapshot. Re-read the current head and
2260
+ // identity/activity before returning positive evidence to local CAS.
2261
+ const head = await this.rpc('chat.history', { sessionKey: key, limit: 20 }, 1500);
2262
+ const hi = head?.sessionInfo, hp = head?.pendingInputs, hid = hi?.sessionId || head?.sessionId;
2263
+ if (!validId(hi?.sessionId) || !validId(head?.sessionId) || head?.sessionKey !== key || hid !== id || hi?.sessionId && head?.sessionId && hi.sessionId !== head.sessionId)
2264
+ return unknown();
2265
+ if (hi?.archived === true)
2266
+ return { ...base, archived: true, idleConfirmed: false };
2267
+ if (hi?.hasActiveRun === true || head?.inFlightRun || Number.isSafeInteger(hp?.queuedCount) && hp.queuedCount > 0)
2268
+ return { ...base, idleConfirmed: false, busyConfirmed: true, activeRunId: typeof head?.inFlightRun?.runId === 'string' ? head.inFlightRun.runId : undefined };
2269
+ if (hp?.total !== total || hp?.queuedCount !== undefined && (!Number.isSafeInteger(hp.queuedCount) || hp.queuedCount < 0))
2270
+ return unknown();
2271
+ if (Array.isArray(hp?.items) && hp.items.some((item) => typeof item?.id === 'string' && item.id && item.state === 'queued'))
2272
+ return { ...base, idleConfirmed: false, busyConfirmed: true };
2273
+ if (!Array.isArray(hp?.items) || JSON.stringify([hid, hp.total, hi?.hasActiveRun, hp.items.map((item) => [item?.id, item?.state]), hp.nextBefore]) !== headSignature)
2274
+ return unknown();
2275
+ }
2276
+ return { ...base, idleConfirmed: true, busyConfirmed: false };
2277
+ }
2278
+ return unknown();
2279
+ };
2126
2280
  try {
2127
- const key = this.canonicalSessionKey(sessionKey);
2128
- const history = await Promise.race([this.rpc('chat.history', { sessionKey: key, limit: 1 }, 1500),
2129
- new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('activity lookup timeout')), 1500); })]);
2130
- const info = history?.sessionInfo;
2131
- return { idleConfirmed: history?.sessionKey === key && info?.hasActiveRun === false &&
2132
- history?.pendingInputs?.total === 0 && !history?.inFlightRun && info?.archived !== true,
2133
- sessionId: info?.sessionId || history?.sessionId, archived: info?.archived === true,
2134
- activeRunId: history?.sessionKey === key && typeof history?.inFlightRun?.runId === 'string' ? history.inFlightRun.runId : undefined,
2135
- busyConfirmed: history?.sessionKey === key && (info?.hasActiveRun === true || !!history?.inFlightRun || history?.pendingInputs?.total > 0) };
2281
+ return await Promise.race([scan(), new Promise((_, reject) => {
2282
+ timer = setTimeout(() => reject(new Error('activity lookup timeout')), 1500);
2283
+ })]);
2136
2284
  }
2137
2285
  catch {
2138
2286
  return { idleConfirmed: false };
@@ -2447,6 +2595,8 @@ export class OpenClawClient {
2447
2595
  ? { state: "stopped", reason: "exact_run_abort_receipt" } : verdict;
2448
2596
  }
2449
2597
  extractTextFromChatMessage(message) {
2598
+ if (this.isInputRecord(message))
2599
+ return "";
2450
2600
  const parts = Array.isArray(message?.content) ? message.content : [];
2451
2601
  return parts.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim();
2452
2602
  }
@@ -3147,6 +3297,7 @@ export class OpenClawClient {
3147
3297
  this.verboseAssistantSent.clear();
3148
3298
  this.verboseAssistantLastTouched.clear();
3149
3299
  this.lastToolNamesByItemId.clear();
3300
+ this.privateInputToolCalls.clear();
3150
3301
  this.recentToolEventKeys.clear();
3151
3302
  this.pendingSteerTexts.clear();
3152
3303
  this.steerConsumedCallbacks.clear();
@@ -0,0 +1,47 @@
1
+ # 1.8.11 candidate — retained pending inputs and output attribution
2
+
3
+ Local offline-tested candidate only. This note does not assert publication,
4
+ runtime activation, real-model execution or Feishu delivery verification.
5
+
6
+ ## Correctness changes
7
+
8
+ - Public `pendingInputs.total` counts retained records, not runnable work.
9
+ Complete, identity-matched `interrupted`/`cancelled` records no longer keep a
10
+ session perpetually busy. Queued inputs, real active runs and in-flight work
11
+ still block ordinary admission. Nothing clears or replays Gateway pending
12
+ records, SQLite business rows or stopped work.
13
+ - Dispatch inspection follows `nextBefore` as `pendingBefore`, with a 20-row
14
+ request limit, a finite page cap and one 1500 ms overall budget. Missing or
15
+ hidden records, malformed states/identities, duplicate IDs/cursors and changed
16
+ page identity/total remain unknown. Multi-page idle proof rechecks the head;
17
+ this is conservative observation, not a transactional Gateway snapshot.
18
+ - Every ordinary handoff uses positive matching dispatch-idle evidence, rather
19
+ than allowing describe-only idle to bypass unknown dispatch state. Existing
20
+ owner/batch/session/stop/admission-window fencing remains in force.
21
+ Proven never-attempted local inputs recover through ordinary paced/startup
22
+ admission; accepted, attempted/unknown and stopped inputs are not replayed.
23
+ - Public transcript forwarding can display an input with `role:assistant`.
24
+ Structured input provenance, sender-session markers and public `:user` input
25
+ identities are now excluded before collector, proactive business delivery
26
+ and assistant progress sinks. Missing runId does not turn an input mirror
27
+ into an assistant reply. Internal send/yield tool bodies are also redacted
28
+ before progress/transcript buffering, including identified wrapper results.
29
+ - Filtering is based on structured input identity, not body-prefix stripping,
30
+ quote matching, external run IDs, or the provenance of the *run's input*.
31
+ Genuine assistant partial/final output, literal quotations of the reported
32
+ wrapper, ordinary external/Cron results and real continuation replies retain
33
+ their existing output paths. Input receipts and SDK acceptance callbacks
34
+ retain their separate acknowledgment semantics.
35
+
36
+ ## Verification boundaries
37
+
38
+ Offline tests exercise the actual WS receiver, Client/Bot collectors, progress
39
+ cards, durable MessageStore/outbox and public SDK fixtures. Synthetic transports
40
+ are explicitly distinguished from an incident raw-frame capture and from live
41
+ platform acceptance. Public producer modules were inspected only as source;
42
+ no private hashed helper was imported or executed and no OpenClaw host file was
43
+ changed. The coordinated `lma-core` plugin remains **0.2.0**.
44
+
45
+ Deployment, activation and live acceptance remain separate owner-controlled
46
+ steps. This candidate does not repair individual historical business rows or
47
+ retry already-delivered/internal input mirrors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.8.10",
3
+ "version": "1.8.11",
4
4
  "description": "Multi-bot Lark/Feishu bridge for OpenClaw, with per-bot model routing and isolated sessions",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -74,7 +74,8 @@
74
74
  "plugins/lma-core/package.json",
75
75
  "plugins/lma-core/README.md",
76
76
  "docs/releases/1.8.5.md",
77
- "docs/releases/1.8.10.md"
77
+ "docs/releases/1.8.10.md",
78
+ "docs/releases/1.8.11.md"
78
79
  ],
79
80
  "engines": {
80
81
  "node": ">=22"