@ycodium-ai/plugin-im 0.2.3102 → 0.2.3112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -126254,6 +126254,169 @@ var keepNewest = (record3, limit) => {
126254
126254
  return entries.length <= limit ? record3 : Object.fromEntries(entries.slice(entries.length - limit));
126255
126255
  };
126256
126256
 
126257
+ // src/boundedMap.ts
126258
+ var makeBoundedMap = (limit) => {
126259
+ const entries = /* @__PURE__ */ new Map();
126260
+ return {
126261
+ get: (key) => entries.get(key),
126262
+ set: (key, value2) => {
126263
+ entries.delete(key);
126264
+ entries.set(key, value2);
126265
+ if (entries.size > limit) {
126266
+ const oldest = entries.keys().next().value;
126267
+ if (oldest !== void 0) entries.delete(oldest);
126268
+ }
126269
+ },
126270
+ /** 忘掉键满足条件的那些。 */
126271
+ forget: (matches) => {
126272
+ for (const key of [...entries.keys()]) if (matches(key)) entries.delete(key);
126273
+ }
126274
+ };
126275
+ };
126276
+
126277
+ // src/replyAnchors.ts
126278
+ var THREAD_LIMIT = 256;
126279
+ var TURNS_PER_THREAD = 16;
126280
+ var anchorOfMessage = (adapter, message) => adapter?.quotesReplies === true ? { messageId: message.messageId, inThread: message.replyInThread === true } : void 0;
126281
+ var userMessageIdOfTurn = (execution, turnId) => {
126282
+ const intent = execution.turnIntents.find((candidate) => candidate.turn?.turnId === turnId);
126283
+ const latest = execution.latestTurn;
126284
+ return intent?.messageId ?? (latest?.turnId === turnId ? latest.userMessageId : void 0);
126285
+ };
126286
+ var makeReplyAnchors = (host) => {
126287
+ const byThread = makeBoundedMap(THREAD_LIMIT);
126288
+ const userMessageOfTurn = async (threadId, turnId) => {
126289
+ const execution = await host.projections.getThreadExecution(threadId).catch(() => null);
126290
+ if (execution === null) return void 0;
126291
+ const wanted = turnId ?? execution.session?.activeTurnId ?? execution.latestTurn?.turnId;
126292
+ return wanted == null ? void 0 : userMessageIdOfTurn(execution, wanted);
126293
+ };
126294
+ return {
126295
+ /**
126296
+ * 开轮时记一笔:`userMessageId` 是派给宿主的那条用户消息,`binding` 是收到聊天消息的那个
126297
+ * 聊天(带着它的锚点)。没有锚点(平台不引用)就不记。
126298
+ */
126299
+ note: (threadId, userMessageId, binding) => {
126300
+ if (binding.reply === void 0) return;
126301
+ const turns = byThread.get(threadId) ?? makeBoundedMap(TURNS_PER_THREAD);
126302
+ byThread.set(threadId, turns);
126303
+ turns.set(userMessageId, { chatKey: chatKeyOf(binding), anchor: binding.reply });
126304
+ },
126305
+ /**
126306
+ * 给发往这条会话的聊天挂上 `turnId` 那一轮的锚点(null 的含义见上);取不到就原样返回。
126307
+ *
126308
+ * 锚点只配给它那条消息所在的聊天:会话可以被另一个聊天接走(`/resume`),而回复接口只按被
126309
+ * 回复的消息寻址,挂错了答复就发回原来的聊天。
126310
+ */
126311
+ anchored: async (binding, threadId, turnId) => {
126312
+ const turns = byThread.get(threadId);
126313
+ if (turns === void 0) return binding;
126314
+ const asked = await userMessageOfTurn(threadId, turnId);
126315
+ const noted = asked === void 0 ? void 0 : turns.get(asked);
126316
+ return noted?.chatKey === chatKeyOf(binding) ? { ...binding, reply: noted.anchor } : binding;
126317
+ }
126318
+ };
126319
+ };
126320
+
126321
+ // src/conclusionLedger.ts
126322
+ var CONCLUSION_DEBT_LIMIT = 500;
126323
+ var keyOf = (threadId, userMessageId) => JSON.stringify([threadId, userMessageId]);
126324
+ var conclusionDebtsOf = (state) => state.conclusions ?? {};
126325
+ var foldConclusionEvent = (state, name, data) => {
126326
+ const threadId = data["threadId"];
126327
+ const userMessageId = data["userMessageId"];
126328
+ if (typeof threadId !== "string" || threadId === "" || typeof userMessageId !== "string" || userMessageId === "") {
126329
+ return state;
126330
+ }
126331
+ const key = keyOf(threadId, userMessageId);
126332
+ const pending = conclusionDebtsOf(state);
126333
+ if (name === "conclusion-delivered") {
126334
+ const { [key]: _delivered, ...rest } = pending;
126335
+ return { ...state, conclusions: rest };
126336
+ }
126337
+ const chatKey = data["chatKey"];
126338
+ const recordedAt = data["recordedAt"];
126339
+ if (typeof chatKey !== "string" || chatKey === "" || typeof recordedAt !== "string" || !Number.isFinite(Date.parse(recordedAt))) {
126340
+ return state;
126341
+ }
126342
+ return {
126343
+ ...state,
126344
+ conclusions: keepNewest(
126345
+ { ...pending, [key]: { chatKey, threadId, userMessageId, recordedAt } },
126346
+ CONCLUSION_DEBT_LIMIT
126347
+ )
126348
+ };
126349
+ };
126350
+ var makeConclusionLedger = (core) => {
126351
+ const inFlight = /* @__PURE__ */ new Set();
126352
+ const registering = /* @__PURE__ */ new Map();
126353
+ const delivered = makeBoundedMap(CONCLUSION_DEBT_LIMIT);
126354
+ let disposed = false;
126355
+ core.host.onDispose(() => {
126356
+ disposed = true;
126357
+ });
126358
+ const settle = (debt) => {
126359
+ delivered.set(keyOf(debt.threadId, debt.userMessageId), true);
126360
+ core.commit("conclusion-delivered", {
126361
+ threadId: debt.threadId,
126362
+ userMessageId: debt.userMessageId
126363
+ });
126364
+ };
126365
+ return {
126366
+ starting: (threadId) => {
126367
+ const gate = Promise.withResolvers();
126368
+ const pending = registering.get(threadId) ?? /* @__PURE__ */ new Set();
126369
+ registering.set(threadId, pending);
126370
+ pending.add(gate.promise);
126371
+ return () => {
126372
+ pending.delete(gate.promise);
126373
+ if (pending.size === 0) registering.delete(threadId);
126374
+ gate.resolve();
126375
+ };
126376
+ },
126377
+ owe: (debt) => {
126378
+ const pending = conclusionDebtsOf(core.state);
126379
+ if (Object.keys(pending).length >= CONCLUSION_DEBT_LIMIT && pending[keyOf(debt.threadId, debt.userMessageId)] === void 0) {
126380
+ core.host.log.warn("chat dropped its oldest conclusion debt at capacity", {
126381
+ oldest: Object.values(pending)[0],
126382
+ limit: CONCLUSION_DEBT_LIMIT
126383
+ });
126384
+ }
126385
+ core.commit("conclusion-owed", { ...debt });
126386
+ },
126387
+ forTurn: async (threadId, turnId = null) => {
126388
+ await Promise.all(registering.get(threadId) ?? []);
126389
+ const execution = await core.host.projections.getThreadExecution(threadId);
126390
+ const userMessageId = execution === null ? void 0 : turnId === null ? execution.latestTurn?.userMessageId : userMessageIdOfTurn(execution, turnId);
126391
+ const key = userMessageId === void 0 ? void 0 : keyOf(threadId, userMessageId);
126392
+ return {
126393
+ debt: key === void 0 ? void 0 : conclusionDebtsOf(core.state)[key],
126394
+ handled: key !== void 0 && (delivered.get(key) === true || inFlight.has(key)),
126395
+ execution
126396
+ };
126397
+ },
126398
+ deliver: async (debt, send) => {
126399
+ const key = keyOf(debt.threadId, debt.userMessageId);
126400
+ if (disposed || inFlight.has(key) || conclusionDebtsOf(core.state)[key] === void 0)
126401
+ return false;
126402
+ inFlight.add(key);
126403
+ try {
126404
+ const sent = await send();
126405
+ if (sent) settle(debt);
126406
+ return sent;
126407
+ } finally {
126408
+ inFlight.delete(key);
126409
+ }
126410
+ },
126411
+ discard: (debt) => {
126412
+ const key = keyOf(debt.threadId, debt.userMessageId);
126413
+ if (!disposed && !inFlight.has(key) && conclusionDebtsOf(core.state)[key] !== void 0) {
126414
+ settle(debt);
126415
+ }
126416
+ }
126417
+ };
126418
+ };
126419
+
126257
126420
  // src/chatState.ts
126258
126421
  var IM_STATE_KEY = "chats";
126259
126422
  var IM_STATE_VERSION = 1;
@@ -126420,6 +126583,9 @@ var withChat = (state, chatKey, patch) => ({
126420
126583
  var foldImEvent = (state, name, data) => {
126421
126584
  if (!isRecord(data)) return state;
126422
126585
  switch (name) {
126586
+ case "conclusion-owed":
126587
+ case "conclusion-delivered":
126588
+ return foldConclusionEvent(state, name, data);
126423
126589
  case "chat-updated": {
126424
126590
  const chatKey = nonEmpty3(data["chatKey"]);
126425
126591
  if (chatKey === null) return state;
@@ -128177,11 +128343,15 @@ var makeImCore = (input) => {
128177
128343
  };
128178
128344
  const deliver = async (binding, text4, attachments = []) => {
128179
128345
  await postImages(binding, attachments);
128180
- if (text4.trim() === "") return;
128346
+ if (text4.trim() === "") return true;
128347
+ const adapter = adapterFor(binding.platform);
128348
+ if (adapter === void 0) return false;
128181
128349
  try {
128182
- await adapterFor(binding.platform)?.sendText(binding, text4);
128350
+ await adapter.sendText(binding, text4);
128351
+ return true;
128183
128352
  } catch (error) {
128184
128353
  host.log.warn("chat could not deliver", { error: describeError(error) });
128354
+ return false;
128185
128355
  }
128186
128356
  };
128187
128357
  const bots = () => readBots(input.config(), adapterFor);
@@ -128209,6 +128379,93 @@ var makeImCore = (input) => {
128209
128379
  };
128210
128380
  };
128211
128381
 
128382
+ // src/conclusionDelivery.ts
128383
+ import { threadOrNull as threadOrNull3 } from "@ycodium-ai/plugin-api";
128384
+
128385
+ // src/threadMessages.ts
128386
+ import {
128387
+ isThreadNotFound,
128388
+ threadMessagesNewestFirst
128389
+ } from "@ycodium-ai/plugin-api";
128390
+ var PAGE_SIZE = 20;
128391
+ async function* newestFirst(threads, threadId) {
128392
+ try {
128393
+ yield* threadMessagesNewestFirst(threads, threadId, { pageSize: PAGE_SIZE });
128394
+ } catch (error) {
128395
+ if (!isThreadNotFound(error)) throw error;
128396
+ }
128397
+ }
128398
+ async function attachmentsOf(threads, threadId, message) {
128399
+ const collected = [...message.attachments ?? []];
128400
+ const total = message.attachmentsTotal ?? collected.length;
128401
+ while (collected.length < total) {
128402
+ let page;
128403
+ try {
128404
+ page = await threads.messages.list({
128405
+ threadId,
128406
+ messageId: message.id,
128407
+ attachmentOffset: collected.length,
128408
+ textLimit: 2
128409
+ });
128410
+ } catch (error) {
128411
+ if (isThreadNotFound(error)) break;
128412
+ throw error;
128413
+ }
128414
+ const more = page.items[0]?.attachments ?? [];
128415
+ if (more.length === 0) break;
128416
+ collected.push(...more);
128417
+ }
128418
+ return collected;
128419
+ }
128420
+ async function latestMessage(threads, threadId, accept) {
128421
+ for await (const message of newestFirst(threads, threadId)) {
128422
+ if (accept(message)) return message;
128423
+ }
128424
+ return void 0;
128425
+ }
128426
+ async function messagesAfterLastUser(threads, threadId) {
128427
+ const after = [];
128428
+ for await (const message of newestFirst(threads, threadId)) {
128429
+ if (message.role === "user") break;
128430
+ after.push(message);
128431
+ }
128432
+ return after.toReversed();
128433
+ }
128434
+
128435
+ // src/conclusionDelivery.ts
128436
+ var makeConclusionDelivery = (core, anchors) => {
128437
+ let disposed = false;
128438
+ core.host.onDispose(() => {
128439
+ disposed = true;
128440
+ });
128441
+ const marked = (text4, recovered) => recovered ? `${core.t("reply.conclusionRecovered")}
128442
+ ${text4}` : text4;
128443
+ return {
128444
+ prepareConclusion: async (threadId, turnId = null) => {
128445
+ const thread = await threadOrNull3(core.host.threads, threadId);
128446
+ const final = thread === null ? void 0 : await latestMessage(
128447
+ core.host.threads,
128448
+ threadId,
128449
+ (message) => message.role === "assistant" && message.text.trim() !== ""
128450
+ );
128451
+ const attachments = final === void 0 ? [] : await attachmentsOf(core.host.threads, threadId, final);
128452
+ return async (binding, options) => {
128453
+ if (disposed || thread === null) return false;
128454
+ if (final === void 0) return options.streaming;
128455
+ const text4 = options.streaming ? "" : marked(`${thread.title}
128456
+
128457
+ ${final.text}`, options.recovered);
128458
+ const anchored = await anchors.anchored(binding, threadId, turnId ?? final.turnId);
128459
+ return !disposed && core.deliver(anchored, text4, attachments);
128460
+ };
128461
+ },
128462
+ failure: async (binding, threadId, notice, recovered, turnId = null) => {
128463
+ const anchored = await anchors.anchored(binding, threadId, turnId);
128464
+ return !disposed && core.deliver(anchored, marked(notice, recovered));
128465
+ }
128466
+ };
128467
+ };
128468
+
128212
128469
  // src/attachments.ts
128213
128470
  var IMAGE_LIMIT_BYTES = 10 * 1024 * 1024;
128214
128471
  var FILE_LIMIT_BYTES = 25 * 1024 * 1024;
@@ -128501,68 +128758,6 @@ var makeCooldown = (windowMs, now, maxKeys = Number.POSITIVE_INFINITY) => {
128501
128758
  };
128502
128759
  };
128503
128760
 
128504
- // src/boundedMap.ts
128505
- var makeBoundedMap = (limit) => {
128506
- const entries = /* @__PURE__ */ new Map();
128507
- return {
128508
- get: (key) => entries.get(key),
128509
- set: (key, value2) => {
128510
- entries.delete(key);
128511
- entries.set(key, value2);
128512
- if (entries.size > limit) {
128513
- const oldest = entries.keys().next().value;
128514
- if (oldest !== void 0) entries.delete(oldest);
128515
- }
128516
- },
128517
- /** 忘掉键满足条件的那些。 */
128518
- forget: (matches) => {
128519
- for (const key of [...entries.keys()]) if (matches(key)) entries.delete(key);
128520
- }
128521
- };
128522
- };
128523
-
128524
- // src/replyAnchors.ts
128525
- var THREAD_LIMIT = 256;
128526
- var TURNS_PER_THREAD = 16;
128527
- var anchorOfMessage = (adapter, message) => adapter?.quotesReplies === true ? { messageId: message.messageId, inThread: message.replyInThread === true } : void 0;
128528
- var makeReplyAnchors = (host) => {
128529
- const byThread = makeBoundedMap(THREAD_LIMIT);
128530
- const userMessageOfTurn = async (threadId, turnId) => {
128531
- const execution = await host.projections.getThreadExecution(threadId).catch(() => null);
128532
- if (execution === null) return void 0;
128533
- const latest = execution.latestTurn;
128534
- const wanted = turnId ?? execution.session?.activeTurnId ?? latest?.turnId;
128535
- if (wanted == null) return void 0;
128536
- const intent = execution.turnIntents.find((candidate) => candidate.turn?.turnId === wanted);
128537
- return intent?.messageId ?? (latest?.turnId === wanted ? latest.userMessageId : void 0);
128538
- };
128539
- return {
128540
- /**
128541
- * 开轮时记一笔:`userMessageId` 是派给宿主的那条用户消息,`binding` 是收到聊天消息的那个
128542
- * 聊天(带着它的锚点)。没有锚点(平台不引用)就不记。
128543
- */
128544
- note: (threadId, userMessageId, binding) => {
128545
- if (binding.reply === void 0) return;
128546
- const turns = byThread.get(threadId) ?? makeBoundedMap(TURNS_PER_THREAD);
128547
- byThread.set(threadId, turns);
128548
- turns.set(userMessageId, { chatKey: chatKeyOf(binding), anchor: binding.reply });
128549
- },
128550
- /**
128551
- * 给发往这条会话的聊天挂上 `turnId` 那一轮的锚点(null 的含义见上);取不到就原样返回。
128552
- *
128553
- * 锚点只配给它那条消息所在的聊天:会话可以被另一个聊天接走(`/resume`),而回复接口只按被
128554
- * 回复的消息寻址,挂错了答复就发回原来的聊天。
128555
- */
128556
- anchored: async (binding, threadId, turnId) => {
128557
- const turns = byThread.get(threadId);
128558
- if (turns === void 0) return binding;
128559
- const asked = await userMessageOfTurn(threadId, turnId);
128560
- const noted = asked === void 0 ? void 0 : turns.get(asked);
128561
- return noted?.chatKey === chatKeyOf(binding) ? { ...binding, reply: noted.anchor } : binding;
128562
- }
128563
- };
128564
- };
128565
-
128566
128761
  // src/inbound.ts
128567
128762
  var SEEN_MESSAGE_LIMIT = 256;
128568
128763
  var ARCHIVED_REPLY_COOLDOWN_MS = 10 * 6e4;
@@ -128600,10 +128795,9 @@ var makeInbound = (core, deps) => {
128600
128795
  });
128601
128796
  if (sender === null) return;
128602
128797
  if (!notAllowedReplyCooldown.tryAcquire(`${target2.rowId}:${sender}`)) return;
128603
- enqueueForChat(
128604
- chatKeyOf(target2),
128605
- () => deliver(target2, t("reply.notAllowedYet", { id: sender }))
128606
- );
128798
+ enqueueForChat(chatKeyOf(target2), async () => {
128799
+ await deliver(target2, t("reply.notAllowedYet", { id: sender }));
128800
+ });
128607
128801
  };
128608
128802
  const currentBot = (rowId) => bots().find((candidate) => candidate.rowId === rowId);
128609
128803
  const inheritFromParent = (bot, chatId, parentChatId) => {
@@ -131703,10 +131897,11 @@ var makeCardStream = (deps) => {
131703
131897
  const run = (async () => {
131704
131898
  try {
131705
131899
  while (pending !== null) {
131706
- const next = pending.view;
131900
+ const next = pending;
131707
131901
  pending = null;
131708
131902
  try {
131709
- await deps.update(id, next);
131903
+ await deps.update(id, next.view);
131904
+ if (next.result !== void 0) next.result.sent = true;
131710
131905
  } catch (error) {
131711
131906
  deps.warn("could not update a card", error);
131712
131907
  }
@@ -131731,12 +131926,14 @@ var makeCardStream = (deps) => {
131731
131926
  }
131732
131927
  },
131733
131928
  finish: async (last) => {
131734
- if (closed) return;
131929
+ if (closed) return false;
131735
131930
  closed = true;
131736
131931
  cancelTimer?.();
131737
131932
  cancelTimer = null;
131738
- pending = { view: last };
131933
+ const result = { sent: false };
131934
+ pending = { view: last, result };
131739
131935
  await drain();
131936
+ return result.sent;
131740
131937
  },
131741
131938
  abandon: () => {
131742
131939
  closed = true;
@@ -138122,6 +138319,7 @@ var en_default = {
138122
138319
  }
138123
138320
  },
138124
138321
  reply: {
138322
+ conclusionRecovered: "(Finished while restarting; delivered now)",
138125
138323
  notAllowed: "You are not on this bot's allow list, so this tap had no effect.",
138126
138324
  noDefaultModel: 'Project "{{title}}" has no agent selected yet, so a thread cannot be started. Send /model to pick one, or set a default in the project settings, then message again.',
138127
138325
  projectArchived: 'Project "{{title}}" is archived. Restore it, or send /repo to pick another project, then message again.',
@@ -138791,6 +138989,7 @@ var zh_CN_default = {
138791
138989
  }
138792
138990
  },
138793
138991
  reply: {
138992
+ conclusionRecovered: "\uFF08\u91CD\u542F\u671F\u95F4\u5B8C\u6210\uFF0C\u73B0\u5728\u8865\u53D1\uFF09",
138794
138993
  notAllowed: "\u4F60\u4E0D\u5728\u8FD9\u4E2A\u673A\u5668\u4EBA\u7684\u5141\u8BB8\u540D\u5355\u91CC\uFF0C\u8FD9\u6B21\u70B9\u51FB\u6CA1\u6709\u751F\u6548\u3002",
138795
138994
  noDefaultModel: "\u9879\u76EE\u300C{{title}}\u300D\u8FD8\u6CA1\u9009 agent\uFF0C\u5F00\u4E0D\u4E86\u4F1A\u8BDD\u3002\u53D1 /model \u9009\u4E00\u4E2A\uFF0C\u6216\u8005\u5230\u9879\u76EE\u8BBE\u7F6E\u91CC\u8BBE\u4E2A\u9ED8\u8BA4\uFF0C\u518D\u53D1\u4E00\u6B21\u6D88\u606F\u3002",
138796
138995
  projectArchived: "\u9879\u76EE\u300C{{title}}\u300D\u5DF2\u5F52\u6863\u3002\u6062\u590D\u5B83\uFF0C\u6216\u53D1 /repo \u6362\u4E2A\u9879\u76EE\uFF0C\u518D\u53D1\u6D88\u606F\u3002",
@@ -139311,14 +139510,14 @@ var makeLiveTranslate = (initial) => {
139311
139510
  };
139312
139511
 
139313
139512
  // src/modelPlan.ts
139314
- import { threadOrNull as threadOrNull3 } from "@ycodium-ai/plugin-api";
139513
+ import { threadOrNull as threadOrNull4 } from "@ycodium-ai/plugin-api";
139315
139514
  var makeModelPlan = (core, deps) => {
139316
139515
  const { host, t, commit, deliver } = core;
139317
139516
  const { threadStillUsable } = deps;
139318
139517
  const currentModelOf = async (binding) => {
139319
139518
  const chat = chatOf(core.state, chatKeyOf(binding));
139320
139519
  if (chat.threadId !== null) {
139321
- const thread = await threadOrNull3(host.threads, chat.threadId).catch(() => null);
139520
+ const thread = await threadOrNull4(host.threads, chat.threadId).catch(() => null);
139322
139521
  if (thread !== null) {
139323
139522
  return {
139324
139523
  instanceId: thread.modelSelection.instanceId,
@@ -139387,7 +139586,62 @@ var makeModelPlan = (core, deps) => {
139387
139586
  };
139388
139587
 
139389
139588
  // src/notifications.ts
139390
- import { readThreadFailed, threadOrNull as threadOrNull4 } from "@ycodium-ai/plugin-api";
139589
+ import { readThreadFailed, threadOrNull as threadOrNull5 } from "@ycodium-ai/plugin-api";
139590
+
139591
+ // src/conclusionRecovery.ts
139592
+ var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
139593
+ var recoverConclusions = async (core, ledger, delivery, failures) => {
139594
+ let disposed = false;
139595
+ core.host.onDispose(() => {
139596
+ disposed = true;
139597
+ });
139598
+ await core.ready;
139599
+ for (const debt of Object.values(conclusionDebtsOf(core.state))) {
139600
+ if (disposed) return;
139601
+ try {
139602
+ const execution = await core.host.projections.getThreadExecution(debt.threadId);
139603
+ if (disposed) return;
139604
+ if (execution === null) {
139605
+ ledger.discard(debt);
139606
+ continue;
139607
+ }
139608
+ const latest = execution.latestTurn;
139609
+ if (latest?.userMessageId !== debt.userMessageId) {
139610
+ core.host.log.info("chat discarded a conclusion whose turn is no longer latest", {
139611
+ ...debt
139612
+ });
139613
+ ledger.discard(debt);
139614
+ continue;
139615
+ }
139616
+ if (latest.state === "running" || execution.session?.activeTurnId != null) continue;
139617
+ if (latest.completedAt !== null && Date.parse(latest.completedAt) < Date.now() - MAX_AGE_MS) {
139618
+ core.host.log.info("chat discarded a conclusion older than seven days", { ...debt });
139619
+ ledger.discard(debt);
139620
+ continue;
139621
+ }
139622
+ const binding = core.bindingForChatKey(debt.chatKey);
139623
+ if (binding === void 0) continue;
139624
+ await ledger.deliver(debt, async () => {
139625
+ if (latest.state === "error") {
139626
+ return delivery.failure(
139627
+ binding,
139628
+ debt.threadId,
139629
+ failures.forFailure(debt.threadId, execution.session?.lastError),
139630
+ true,
139631
+ latest.turnId
139632
+ );
139633
+ }
139634
+ const send = await delivery.prepareConclusion(debt.threadId, latest.turnId);
139635
+ return send(binding, { recovered: true, streaming: false });
139636
+ });
139637
+ } catch (error) {
139638
+ core.host.log.warn("chat could not recover a conclusion", {
139639
+ ...debt,
139640
+ error: describeError(error)
139641
+ });
139642
+ }
139643
+ }
139644
+ };
139391
139645
 
139392
139646
  // src/notificationTargets.ts
139393
139647
  var notificationChatKeys = (state, threadId, projectId) => {
@@ -139396,56 +139650,6 @@ var notificationChatKeys = (state, threadId, projectId) => {
139396
139650
  return projectId === null ? [] : chatsOnProject(state, projectId);
139397
139651
  };
139398
139652
 
139399
- // src/threadMessages.ts
139400
- import {
139401
- isThreadNotFound,
139402
- threadMessagesNewestFirst
139403
- } from "@ycodium-ai/plugin-api";
139404
- var PAGE_SIZE = 20;
139405
- async function* newestFirst(threads, threadId) {
139406
- try {
139407
- yield* threadMessagesNewestFirst(threads, threadId, { pageSize: PAGE_SIZE });
139408
- } catch (error) {
139409
- if (!isThreadNotFound(error)) throw error;
139410
- }
139411
- }
139412
- async function attachmentsOf(threads, threadId, message) {
139413
- const collected = [...message.attachments ?? []];
139414
- const total = message.attachmentsTotal ?? collected.length;
139415
- while (collected.length < total) {
139416
- let page;
139417
- try {
139418
- page = await threads.messages.list({
139419
- threadId,
139420
- messageId: message.id,
139421
- attachmentOffset: collected.length,
139422
- textLimit: 2
139423
- });
139424
- } catch (error) {
139425
- if (isThreadNotFound(error)) break;
139426
- throw error;
139427
- }
139428
- const more = page.items[0]?.attachments ?? [];
139429
- if (more.length === 0) break;
139430
- collected.push(...more);
139431
- }
139432
- return collected;
139433
- }
139434
- async function latestMessage(threads, threadId, accept) {
139435
- for await (const message of newestFirst(threads, threadId)) {
139436
- if (accept(message)) return message;
139437
- }
139438
- return void 0;
139439
- }
139440
- async function messagesAfterLastUser(threads, threadId) {
139441
- const after = [];
139442
- for await (const message of newestFirst(threads, threadId)) {
139443
- if (message.role === "user") break;
139444
- after.push(message);
139445
- }
139446
- return after.toReversed();
139447
- }
139448
-
139449
139653
  // src/turnOutcome.ts
139450
139654
  var outcomeOfSession = (status) => {
139451
139655
  if (status === "error") return "failed";
@@ -139456,15 +139660,16 @@ var outcomeOfSession = (status) => {
139456
139660
  // src/notifications.ts
139457
139661
  var THREAD_PROJECT_CACHE_LIMIT = 256;
139458
139662
  var makeNotifications = (core, deps) => {
139459
- const { host, adapterFor, deliver, bindingForChatKey, ready } = core;
139663
+ const { host, adapterFor, bindingForChatKey, ready } = core;
139460
139664
  const { clearWorking } = deps;
139461
139665
  const { turnCards } = deps.replyCards;
139462
- const { failures, anchors } = deps;
139666
+ const { failures, conclusions, delivery } = deps;
139667
+ void recoverConclusions(core, conclusions, delivery, failures);
139463
139668
  const projectByThread = /* @__PURE__ */ new Map();
139464
139669
  const projectOfThread = async (threadId) => {
139465
139670
  const known = projectByThread.get(threadId);
139466
139671
  if (known !== void 0) return known;
139467
- const thread = await threadOrNull4(host.threads, threadId);
139672
+ const thread = await threadOrNull5(host.threads, threadId);
139468
139673
  if (thread === null) return null;
139469
139674
  if (projectByThread.size >= THREAD_PROJECT_CACHE_LIMIT) {
139470
139675
  const oldest = projectByThread.keys().next().value;
@@ -139501,26 +139706,28 @@ var makeNotifications = (core, deps) => {
139501
139706
  const said = threadsWithSomethingToReport.delete(threadId);
139502
139707
  await ready;
139503
139708
  const targets = targetsForProject(threadId, await projectOfThread(threadId));
139504
- const reporters = said ? targets : [];
139505
- if (reporters.length > 0) {
139506
- const thread = await threadOrNull4(host.threads, threadId);
139507
- const final = thread === null ? void 0 : await latestMessage(
139508
- host.threads,
139509
- threadId,
139510
- (message) => message.role === "assistant" && message.text.trim() !== ""
139511
- );
139512
- if (thread !== null && final !== void 0) {
139513
- const attachments = await attachmentsOf(host.threads, threadId, final);
139514
- for (const binding of reporters) {
139515
- const text4 = streamsTo(binding, threadId) ? "" : `${thread.title}
139516
-
139517
- ${final.text}`;
139518
- await deliver(
139519
- owns(binding, threadId) ? await anchors.anchored(binding, threadId, final.turnId) : binding,
139520
- text4,
139521
- attachments
139709
+ const owes = Object.values(conclusionDebtsOf(core.state)).some(
139710
+ (debt2) => debt2.threadId === threadId
139711
+ );
139712
+ const { debt, handled, execution } = said || owes ? await conclusions.forTurn(threadId) : { debt: void 0, handled: false, execution: null };
139713
+ if (debt !== void 0) {
139714
+ const binding = bindingForChatKey(debt.chatKey);
139715
+ if (binding !== void 0 && !streamsTo(binding, threadId) && execution?.session?.activeTurnId == null && execution?.latestTurn?.state !== "running" && execution?.latestTurn?.state !== "error" && payload.session?.status !== "error") {
139716
+ await conclusions.deliver(debt, async () => {
139717
+ const send = await delivery.prepareConclusion(
139718
+ threadId,
139719
+ execution?.latestTurn?.turnId ?? null
139522
139720
  );
139523
- }
139721
+ return send(binding, { recovered: false, streaming: false });
139722
+ });
139723
+ }
139724
+ } else if (said && !handled) {
139725
+ const send = await delivery.prepareConclusion(threadId);
139726
+ for (const binding of targets) {
139727
+ await send(binding, {
139728
+ recovered: false,
139729
+ streaming: streamsTo(binding, threadId)
139730
+ });
139524
139731
  }
139525
139732
  }
139526
139733
  const outcome = outcomeOfSession(payload.session?.status);
@@ -139533,7 +139740,10 @@ ${final.text}`;
139533
139740
  threadsWithSomethingToReport.delete(threadId);
139534
139741
  await ready;
139535
139742
  const targets = targetsForProject(threadId, await projectOfThread(threadId));
139536
- const textTargets = targets.filter(
139743
+ const { debt, handled } = await conclusions.forTurn(threadId);
139744
+ const debtBinding = debt === void 0 ? void 0 : bindingForChatKey(debt.chatKey);
139745
+ const recipients = debt !== void 0 ? debtBinding === void 0 ? [] : [debtBinding] : targets;
139746
+ const textTargets = handled ? [] : recipients.filter(
139537
139747
  (binding) => !(streamsTo(binding, threadId) && turnCards.covers(threadId))
139538
139748
  );
139539
139749
  const drawingTurn = turnCards.drawingTurn(threadId);
@@ -139542,7 +139752,13 @@ ${final.text}`;
139542
139752
  failures.holdForCard(threadId, drawingTurn, notice);
139543
139753
  for (const binding of targets) {
139544
139754
  await clearWorking(binding, threadId, "failed");
139545
- if (notice !== null && textTargets.includes(binding)) await deliver(binding, notice);
139755
+ }
139756
+ if (notice !== null) {
139757
+ for (const binding of textTargets) {
139758
+ const send = () => delivery.failure(binding, threadId, notice, false);
139759
+ if (debt !== void 0) await conclusions.deliver(debt, send);
139760
+ else await send();
139761
+ }
139546
139762
  }
139547
139763
  });
139548
139764
  const rememberThreadProject = (threadId, projectId) => {
@@ -140135,7 +140351,7 @@ var makeFailureNotices = (core) => {
140135
140351
  const heldForCard = /* @__PURE__ */ new Map();
140136
140352
  const reasonOf2 = (detail) => detail?.trim() ?? "";
140137
140353
  return {
140138
- /** 一次失败事件的那句话(带新参考号,同号进日志)。 */
140354
+ /** 一次失败通知的那句话(带新参考号,同号进日志):实时失败事件与重启补发各取一次。 */
140139
140355
  forFailure: (threadId, detail) => {
140140
140356
  const reference2 = diagnosticRef("msg");
140141
140357
  core.host.log.warn("chat turn failed", { threadId, reference: reference2 });
@@ -140165,7 +140381,7 @@ var makeFailureNotices = (core) => {
140165
140381
  // src/replyCards.ts
140166
140382
  import {
140167
140383
  readTurnProgress,
140168
- threadOrNull as threadOrNull5,
140384
+ threadOrNull as threadOrNull6,
140169
140385
  TURN_PROGRESS_TRIGGER_KIND
140170
140386
  } from "@ycodium-ai/plugin-api";
140171
140387
 
@@ -140291,15 +140507,19 @@ var makeTurnCards = (deps) => {
140291
140507
  const title = live?.title ?? await deps.titleOf(threadId);
140292
140508
  const view = await finalView(title, progress, live, end);
140293
140509
  if (live?.card != null) {
140294
- await live.card.finish(view);
140295
- deps.onClosed(live.card.cardId);
140296
- return;
140297
- }
140298
- const card = await openCard(target2, view);
140299
- if (card === null) {
140300
- await deps.fallback(threadId, turnId, target2.chatKey, view);
140510
+ const card = live.card;
140511
+ const sent = await card.finish(view);
140512
+ deps.onClosed(card.cardId);
140513
+ if (deps.finalize !== void 0)
140514
+ await deps.finalize(threadId, turnId, target2.chatKey, async () => sent);
140301
140515
  return;
140302
140516
  }
140517
+ const send = async () => {
140518
+ const card = await openCard(target2, view);
140519
+ return card !== null || await deps.fallback(threadId, turnId, target2.chatKey, view);
140520
+ };
140521
+ if (deps.finalize === void 0) await send();
140522
+ else await deps.finalize(threadId, turnId, target2.chatKey, send);
140303
140523
  };
140304
140524
  const handle = async (slot, threadId, item) => {
140305
140525
  await deps.ready;
@@ -140444,7 +140664,7 @@ var makeTurnCards = (deps) => {
140444
140664
  };
140445
140665
 
140446
140666
  // src/replyCards.ts
140447
- var makeReplyCards = (core, failures, anchors) => {
140667
+ var makeReplyCards = (core, failures, anchors, conclusions, delivery) => {
140448
140668
  const { host, adapterFor, commit, deliver, bindingForChatKey, bindingOfThread, ready } = core;
140449
140669
  const stopRequested = /* @__PURE__ */ new Set();
140450
140670
  const turnCards = makeTurnCards({
@@ -140480,12 +140700,12 @@ var makeReplyCards = (core, failures, anchors) => {
140480
140700
  (message) => message.role === "user" && message.text.trim() !== ""
140481
140701
  ).catch(() => void 0);
140482
140702
  if (asked !== void 0) return turnTitleOf(asked.text);
140483
- const thread = await threadOrNull5(host.threads, threadId).catch(() => null);
140703
+ const thread = await threadOrNull6(host.threads, threadId).catch(() => null);
140484
140704
  return turnTitleOf(thread?.title ?? "");
140485
140705
  },
140486
140706
  endOf: async (threadId, turnId) => {
140487
140707
  const stopped = stopRequested.delete(threadId);
140488
- const thread = await threadOrNull5(host.threads, threadId).catch(() => null);
140708
+ const thread = await threadOrNull6(host.threads, threadId).catch(() => null);
140489
140709
  const session = thread?.session;
140490
140710
  const outcome = outcomeOfSession(session?.status);
140491
140711
  const errorText2 = failures.forCard(
@@ -140502,11 +140722,24 @@ var makeReplyCards = (core, failures, anchors) => {
140502
140722
  },
140503
140723
  onOpened: (card) => commit("turn-card-opened", card),
140504
140724
  onClosed: (cardId) => commit("turn-card-closed", { cardId }),
140505
- fallback: async (threadId, turnId, chatKey, view) => {
140725
+ finalize: async (threadId, turnId, chatKey, send) => {
140726
+ const { debt, handled } = await conclusions.forTurn(threadId, turnId);
140727
+ if (handled) return;
140728
+ if (debt === void 0) {
140729
+ await send();
140730
+ return;
140731
+ }
140732
+ if (!await conclusions.deliver(debt, send)) return;
140506
140733
  const binding = bindingForChatKey(chatKey);
140507
140734
  if (binding === void 0) return;
140735
+ const sendConclusion = await delivery.prepareConclusion(threadId, turnId);
140736
+ await sendConclusion(binding, { recovered: false, streaming: true });
140737
+ },
140738
+ fallback: async (threadId, turnId, chatKey, view) => {
140739
+ const binding = bindingForChatKey(chatKey);
140740
+ if (binding === void 0) return false;
140508
140741
  const text4 = [view.body, view.errorText ?? ""].filter((part) => part !== "").join("\n\n");
140509
- await deliver(await anchors.anchored(binding, threadId, turnId), text4);
140742
+ return deliver(await anchors.anchored(binding, threadId, turnId), text4);
140510
140743
  },
140511
140744
  schedule: (run, delayMs) => {
140512
140745
  const timer = setTimeout(run, delayMs);
@@ -140724,7 +140957,7 @@ var makeSendFileTool = (deps) => ({
140724
140957
  });
140725
140958
 
140726
140959
  // src/sessions.ts
140727
- import { threadOrNull as threadOrNull6 } from "@ycodium-ai/plugin-api";
140960
+ import { threadOrNull as threadOrNull7 } from "@ycodium-ai/plugin-api";
140728
140961
  var WEB_LIST_PAGE = 100;
140729
140962
  var shellOf = (thread) => ({
140730
140963
  threadId: thread.id,
@@ -140784,7 +141017,7 @@ var makeSessions = (core) => {
140784
141017
  });
140785
141018
  };
140786
141019
  const rowForThread = async (binding, chatKey, threadId) => {
140787
- const thread = await threadOrNull6(host.threads, threadId, { archived: "include" });
141020
+ const thread = await threadOrNull7(host.threads, threadId, { archived: "include" });
140788
141021
  if (thread === null || thread.projectId !== binding.projectId) return null;
140789
141022
  return rowFromThread(shellOf(thread), chatKey, originOf(threadId, binding.projectId));
140790
141023
  };
@@ -141938,7 +142171,7 @@ ${line}`
141938
142171
  };
141939
142172
 
141940
142173
  // src/turns.ts
141941
- import { projectOrNull as projectOrNull2, threadOrNull as threadOrNull7 } from "@ycodium-ai/plugin-api";
142174
+ import { projectOrNull as projectOrNull2, threadOrNull as threadOrNull8 } from "@ycodium-ai/plugin-api";
141942
142175
  var TITLE_MAX_CHARS = 72;
141943
142176
  var titleFrom = (text4) => text4.replace(/\s+/g, " ").slice(0, TITLE_MAX_CHARS) || "Chat thread";
141944
142177
  var makeTurns = (core, deps) => {
@@ -141947,7 +142180,7 @@ var makeTurns = (core, deps) => {
141947
142180
  const { pendingByChat, pendingFor, collectResources } = deps.attachments;
141948
142181
  const threadStillUsable = async (binding, threadId) => {
141949
142182
  try {
141950
- const thread = await threadOrNull7(host.threads, threadId, { archived: "include" });
142183
+ const thread = await threadOrNull8(host.threads, threadId, { archived: "include" });
141951
142184
  return thread !== null && thread.archivedAt == null && thread.projectId === binding.projectId;
141952
142185
  } catch {
141953
142186
  return true;
@@ -142045,6 +142278,7 @@ var makeTurns = (core, deps) => {
142045
142278
  } catch {
142046
142279
  }
142047
142280
  }
142281
+ const registered = deps.conclusions.starting(threadId);
142048
142282
  try {
142049
142283
  await host.threads.turns.start({
142050
142284
  requestId: crypto.randomUUID(),
@@ -142062,9 +142296,19 @@ var makeTurns = (core, deps) => {
142062
142296
  interactionMode: interactionModeOf(chatKey),
142063
142297
  ...intentAction !== void 0 ? { intentAction } : {}
142064
142298
  });
142299
+ if (intentAction?.kind !== "steer") {
142300
+ deps.conclusions.owe({
142301
+ chatKey,
142302
+ threadId,
142303
+ userMessageId,
142304
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
142305
+ });
142306
+ }
142065
142307
  } catch (error) {
142066
142308
  if (carried !== void 0) pendingByChat.set(chatKey, carried);
142067
142309
  throw error;
142310
+ } finally {
142311
+ registered();
142068
142312
  }
142069
142313
  deps.noteInboundUserMessage?.(userMessageId);
142070
142314
  host.log.info("chat started a turn from a message", {
@@ -144473,13 +144717,22 @@ var imPlugin = definePlugin({
144473
144717
  const acknowledgements = makeAcknowledgements(core);
144474
144718
  const failures = makeFailureNotices(core);
144475
144719
  const replyAnchors = makeReplyAnchors(core.host);
144476
- const replyCards = makeReplyCards(core, failures, replyAnchors);
144720
+ const conclusions = makeConclusionLedger(core);
144721
+ const conclusionDelivery = makeConclusionDelivery(core, replyAnchors);
144722
+ const replyCards = makeReplyCards(
144723
+ core,
144724
+ failures,
144725
+ replyAnchors,
144726
+ conclusions,
144727
+ conclusionDelivery
144728
+ );
144477
144729
  const teams = makeTeamGoals(core);
144478
144730
  const notifications = makeNotifications(core, {
144479
144731
  clearWorking: acknowledgements.clearWorking,
144480
144732
  replyCards,
144481
144733
  failures,
144482
- anchors: replyAnchors
144734
+ conclusions,
144735
+ delivery: conclusionDelivery
144483
144736
  });
144484
144737
  const webSync = makeWebSync(core);
144485
144738
  const approvals = makeApprovals(core, replyAnchors);
@@ -144490,6 +144743,7 @@ var imPlugin = definePlugin({
144490
144743
  });
144491
144744
  const inboundAttachments = makeInboundAttachments(core);
144492
144745
  const turns = makeTurns(core, {
144746
+ conclusions,
144493
144747
  attachments: inboundAttachments,
144494
144748
  queueFlow: interactions.queueFlow,
144495
144749
  rememberThreadProject: notifications.rememberThreadProject,
package/locales/en.json CHANGED
@@ -193,6 +193,7 @@
193
193
  }
194
194
  },
195
195
  "reply": {
196
+ "conclusionRecovered": "(Finished while restarting; delivered now)",
196
197
  "notAllowed": "You are not on this bot's allow list, so this tap had no effect.",
197
198
  "noDefaultModel": "Project \"{{title}}\" has no agent selected yet, so a thread cannot be started. Send /model to pick one, or set a default in the project settings, then message again.",
198
199
  "projectArchived": "Project \"{{title}}\" is archived. Restore it, or send /repo to pick another project, then message again.",
@@ -193,6 +193,7 @@
193
193
  }
194
194
  },
195
195
  "reply": {
196
+ "conclusionRecovered": "(重启期间完成,现在补发)",
196
197
  "notAllowed": "你不在这个机器人的允许名单里,这次点击没有生效。",
197
198
  "noDefaultModel": "项目「{{title}}」还没选 agent,开不了会话。发 /model 选一个,或者到项目设置里设个默认,再发一次消息。",
198
199
  "projectArchived": "项目「{{title}}」已归档。恢复它,或发 /repo 换个项目,再发消息。",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-im",
3
- "version": "0.2.3102",
3
+ "version": "0.2.3112",
4
4
  "description": "Talk to your agents from Telegram, WeCom, Lark, or WeChat (experimental).",
5
5
  "keywords": [
6
6
  "im",
@@ -32,7 +32,7 @@
32
32
  "markdown-it": "^15.0.2",
33
33
  "react": "19.2.6",
34
34
  "vite-plus": "0.2.2",
35
- "@ycodium-ai/plugin-api": "0.2.3102"
35
+ "@ycodium-ai/plugin-api": "0.2.3112"
36
36
  },
37
37
  "ycodium": {
38
38
  "plugin": "./dist/index.js",