toolroll 0.9.16 → 0.9.17

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/assignment-brief.js +11 -4
  3. package/dist/chat-actions.d.ts +7 -0
  4. package/dist/chat-actions.js +40 -4
  5. package/dist/chat-ask.js +29 -15
  6. package/dist/chat-channel.js +3 -1
  7. package/dist/chat-delivery-state.d.ts +2 -0
  8. package/dist/chat-delivery.d.ts +3 -0
  9. package/dist/chat-delivery.js +31 -16
  10. package/dist/chat-warmth.d.ts +21 -0
  11. package/dist/chat-warmth.js +36 -0
  12. package/dist/discord-api.d.ts +1 -1
  13. package/dist/discord-api.js +1 -1
  14. package/dist/discord-chat.d.ts +3 -2
  15. package/dist/discord-chat.js +18 -3
  16. package/dist/lead-about.d.ts +35 -0
  17. package/dist/lead-about.js +62 -0
  18. package/dist/lead-capabilities.d.ts +64 -0
  19. package/dist/lead-capabilities.js +199 -0
  20. package/dist/lead-commitments.js +13 -7
  21. package/dist/lead-context.d.ts +5 -3
  22. package/dist/lead-context.js +29 -7
  23. package/dist/lead-follow.d.ts +1 -1
  24. package/dist/lead-follow.js +3 -2
  25. package/dist/lead-identity.js +1 -1
  26. package/dist/lead-people.d.ts +55 -0
  27. package/dist/lead-people.js +128 -0
  28. package/dist/lead-voice.d.ts +3 -2
  29. package/dist/lead-voice.js +4 -3
  30. package/dist/mate-cli.d.ts +3 -0
  31. package/dist/mate-cli.js +15 -14
  32. package/dist/mate-contract.d.ts +1 -1
  33. package/dist/mate-contract.js +6 -4
  34. package/dist/mate-progress.js +2 -0
  35. package/dist/mate-tools.d.ts +8 -0
  36. package/dist/mate-tools.js +112 -6
  37. package/dist/mate.d.ts +26 -1
  38. package/dist/mate.js +89 -25
  39. package/dist/operate.js +2 -0
  40. package/dist/phase-routing.d.ts +1 -1
  41. package/dist/provider-auth.d.ts +4 -0
  42. package/dist/provider-auth.js +1 -1
  43. package/dist/reply-shape.d.ts +96 -0
  44. package/dist/reply-shape.js +452 -0
  45. package/dist/serve.js +53 -28
  46. package/dist/slack-chat.d.ts +2 -0
  47. package/dist/slack-chat.js +20 -3
  48. package/dist/store.d.ts +12 -3
  49. package/dist/store.js +44 -9
  50. package/dist/team-runtime.js +1 -1
  51. package/dist/teams-chat.js +12 -1
  52. package/dist/telegram-mate.d.ts +2 -0
  53. package/dist/telegram-mate.js +27 -11
  54. package/dist/telegram-team.js +6 -3
  55. package/dist/telegram.js +1 -1
  56. package/dist/test-temp.js +1 -1
  57. package/package.json +1 -1
@@ -23,11 +23,13 @@ export { channelRepos as telegramConversationRepos, resolveChannelMate as resolv
23
23
  * the console and CLI state), and the door re-proves everything again.
24
24
  */
25
25
  import { createHash, randomBytes } from "node:crypto";
26
- import { MATE_MESSAGE_MAX_CHARS, runMateTurn } from "./mate.js";
26
+ import { MATE_MESSAGE_MAX_CHARS, mateFailureText, runMateTurn } from "./mate.js";
27
27
  import { confirmMateProposal, dismissMateProposal } from "./mate-doors.js";
28
28
  import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
29
29
  import { MATE_ASK_OTHER } from "./store.js";
30
30
  import { phoneText } from "./telegram-status.js";
31
+ import { shapeReplyParts, telegramReply } from "./reply-shape.js";
32
+ import { WARM_EMOJI, warmTurn } from "./chat-warmth.js";
31
33
  import { TeamLeads } from "./team-leads.js";
32
34
  import { chatResultHref } from "./chat-controls.js";
33
35
  import { resultImageFileName, safeResultImageCaption, verifyResultImage } from "./chat-evidence.js";
@@ -40,7 +42,7 @@ export const CONVERSATION_MAX_AGE_MS = 10 * 60_000;
40
42
  export const CARD_TTL_MS = 24 * 3_600_000;
41
43
  export const CHALLENGE_TTL_MS = 10 * 60_000;
42
44
  /** Telegram's own message ceiling, with room for our part headers (the same bound telegram.ts splits at). */
43
- const PART_CAP = 3_900;
45
+ export const PART_CAP = 3_900;
44
46
  // ---- the ceiling and the principal --------------------------------------------
45
47
  /**
46
48
  * The phone's project ceiling: the enrolled registry, canonicalized and
@@ -405,7 +407,7 @@ async function runTelegramConversation(row, args) {
405
407
  const bound = store.getTelegramConversation(row.id)?.session ?? null;
406
408
  const session = bound === null ? null : store.getMateSession(bound);
407
409
  if (session === null) {
408
- await notify(couldNotAnswerText("the chat session for this reply is gone"));
410
+ await notify("This conversation was restarted before my reply went out, so it wasn't sent. Nothing was changed. Send your message again.");
409
411
  finish({ state: "failed", outcome: "unsent:no-session" });
410
412
  report.refused++;
411
413
  return;
@@ -525,11 +527,14 @@ async function runTelegramConversation(row, args) {
525
527
  const origin = proposal === null ? null : options.phoneOrigin?.() ?? null;
526
528
  const wanted = proposal === null ? null : proposalLink(store, proposal, repos);
527
529
  const keyboard = keyboardWith(part.keyboard, phoneLinkButton(origin, wanted));
528
- const text = proposal !== null && part.keyboard === null ? handoffCardText(part.text, linkNote(origin, wanted)) : part.text;
530
+ // The lead's own words carry their bold anchors and labelled links as entities, never as Markdown characters.
531
+ const voiced = part.kind === "reply" ? telegramReply(part.text) : null;
532
+ const text = proposal !== null && part.keyboard === null ? handoffCardText(part.text, linkNote(origin, wanted)) : voiced?.text ?? part.text;
529
533
  method = "sendMessage";
530
534
  send = () => transport("sendMessage", {
531
535
  chat_id: chatId,
532
536
  text,
537
+ ...(voiced === null || voiced.entities.length === 0 ? {} : { entities: voiced.entities }),
533
538
  link_preview_options: { is_disabled: true },
534
539
  ...(part.replyTo === null ? {} : { reply_parameters: { message_id: Number(part.replyTo) } }),
535
540
  ...(keyboard === undefined ? {} : { reply_markup: { inline_keyboard: keyboard } }),
@@ -590,7 +595,7 @@ async function runTelegramConversation(row, args) {
590
595
  try {
591
596
  return store.transact(() => {
592
597
  const now = clock();
593
- const parts = splitParts(reply).map((text, index) => ({ kind: "reply", text, replyTo: index === 0 ? row.messageId : null }));
598
+ const parts = shapeReplyParts(reply, PART_CAP, { asked: row.text, appOrigin: options.phoneOrigin?.() ?? null }, shaped => telegramReply(shaped).text.length).map((text, index) => ({ kind: "reply", text, replyTo: index === 0 ? row.messageId : null }));
594
599
  // The lead's question to its owner: one tap per option, then "Something else".
595
600
  const ask = store.mateAsk(turn);
596
601
  if (ask !== null)
@@ -633,7 +638,7 @@ async function runTelegramConversation(row, args) {
633
638
  return;
634
639
  }
635
640
  if (turn.state !== "answered") {
636
- await notify(couldNotAnswerText(`the assistant's reply did not complete (${phoneText(turn.failureReason ?? "unknown", 40)})`));
641
+ await notify(mateFailureText(turn.failureReason));
637
642
  finish({ state: "failed", outcome: `replayed:${turn.failureReason ?? "failed"}` });
638
643
  report.refused++;
639
644
  return;
@@ -693,7 +698,13 @@ async function runTelegramConversation(row, args) {
693
698
  const { who, session, thread, config } = resolved;
694
699
  const revalidate = async () => {
695
700
  const problem = await telegramChannelProblem(store, { botId, bindingId: binding.id, approverGeneration: binding.approverGeneration, repos: who.repos }, readProjects);
696
- return problem === null ? { ok: true } : { ok: false, reason: problem };
701
+ if (problem === null)
702
+ return { ok: true };
703
+ const reason = problem === UNREADABLE_REGISTRY ? "projects-unreadable"
704
+ : problem === "this chat is no longer paired" ? "unpaired"
705
+ : problem === "the paired account is no longer an approver" ? "not-approver"
706
+ : problem === "the connected projects changed" ? "projects-changed" : "access-changed";
707
+ return { ok: false, reason };
697
708
  };
698
709
  // Bound BEFORE the dispatch, under the claim: a crash from here on finds
699
710
  // the receipt in this session, not in whichever session is live later.
@@ -704,17 +715,23 @@ async function runTelegramConversation(row, args) {
704
715
  // crash gets the original turn back, never a second dispatch.
705
716
  const heartbeat = setInterval(() => { held(); }, 30_000);
706
717
  heartbeat.unref?.();
718
+ // Once the turn reaches a tool step: a 👍 on the owner's message, and "typing…" while the lead works.
719
+ const warm = warmTurn({
720
+ react: () => transport("setMessageReaction", { chat_id: chatId, message_id: Number(row.messageId), reaction: [{ type: "emoji", emoji: WARM_EMOJI }] }),
721
+ typing: () => transport("sendChatAction", { chat_id: chatId, action: "typing" }),
722
+ });
707
723
  let outcome;
708
724
  try {
709
725
  outcome = await runMateTurn({
710
726
  store, who, session, thread, config, key: null, message: row.text, requestId: row.request,
711
727
  ...(row.context === null ? {} : { context: row.context }),
712
728
  ...(options.subscriptionRunner === undefined ? {} : { subscriptionRunner: options.subscriptionRunner }),
713
- clock, evidenceRoot: options.evidenceRoot, revalidate, mediaDelivery: "documents", channel: "telegram",
729
+ clock, evidenceRoot: options.evidenceRoot, revalidate, mediaDelivery: "documents", channel: "telegram", onProgress: warm.onProgress,
714
730
  });
715
731
  }
716
732
  finally {
717
733
  clearInterval(heartbeat);
734
+ warm.stop();
718
735
  }
719
736
  const receipt = store.mateRequestReceipt(session.id, row.request);
720
737
  if (receipt !== null)
@@ -724,7 +741,7 @@ async function runTelegramConversation(row, args) {
724
741
  await requeue("busy");
725
742
  return;
726
743
  }
727
- await notify(phoneText(couldNotAnswerText(outcome.message), 1_000));
744
+ await notify(phoneText(outcome.message, 1_000));
728
745
  finish({ state: "failed", outcome: `refused:${outcome.refused}` });
729
746
  report.refused++;
730
747
  return;
@@ -732,8 +749,7 @@ async function runTelegramConversation(row, args) {
732
749
  if (!outcome.ok) {
733
750
  // Truthful: what failed, that nothing was kept, and that a new message
734
751
  // is a new turn. A failed turn's drafts are already gone.
735
- const words = outcome.failed === "revoked" ? `${outcome.message.charAt(0).toUpperCase()}${outcome.message.slice(1)}.` : couldNotAnswerText(`the assistant's reply did not complete (${outcome.message})`);
736
- await notify(phoneText(words, 1_000));
752
+ await notify(phoneText(outcome.message, 1_000));
737
753
  finish({ state: "failed", outcome: `failed:${outcome.failed}` });
738
754
  report.refused++;
739
755
  return;
@@ -1,6 +1,7 @@
1
+ import { renderReply, shapeReplyParts } from "./reply-shape.js";
1
2
  import { TeamLeads } from "./team-leads.js";
2
3
  import { proposalPreview, tooLongText } from "./chat-channel.js";
3
- import { mintCardTokens, phoneLinkButton, splitParts } from "./telegram-mate.js";
4
+ import { PART_CAP, mintCardTokens, phoneLinkButton, splitParts } from "./telegram-mate.js";
4
5
  import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
5
6
  import { phoneText } from "./telegram-status.js";
6
7
  /** `/team` lists, `/team <n>` chooses, `/team off` (or `private`) leaves. Any other slash text is not ours. */
@@ -265,7 +266,9 @@ export async function deliverTeamChats(store, botId, transport, clock, report, p
265
266
  const skip = role === "operator" && (ownMessage || status === "cancelled" || String(message["author"] ?? "").length === 0);
266
267
  const prefix = `telegram-team:${chat.id}:`;
267
268
  if (!skip) {
268
- const text = role === "assistant" ? String(message["text"]) : `${phoneText(String(message["author"]), 40)}: ${String(message["text"])}`;
269
+ // The lead's reply is split as written, then each part shaped and measured as Telegram shows it, so no cut lands
270
+ // inside a link and no rendered part passes Telegram's ceiling.
271
+ const text = role === "assistant" ? shapeReplyParts(String(message["text"]), PART_CAP, { appOrigin: phoneOrigin?.() ?? null }, shaped => renderReply(shaped, "telegram").length).map(part => renderReply(part, "telegram")) : `${phoneText(String(message["author"]), 40)}: ${String(message["text"])}`;
269
272
  const sent = await sendParts(store, transport, chat.chatId, text, `${prefix}message:${id}`, clock, access, report);
270
273
  if (sent !== null) {
271
274
  report.problems.push(`team chat ${chat.chatId}: ${sent}`);
@@ -329,7 +332,7 @@ function cardBindingFor(store, botId, chat, turn) {
329
332
  return bindings.find(one => members.has(one.approver)) ?? null;
330
333
  }
331
334
  async function sendParts(store, transport, chatId, text, receipt, clock, access, report, keyboard, tokens = []) {
332
- const parts = splitParts(text);
335
+ const parts = typeof text === "string" ? splitParts(text) : text;
333
336
  const delivered = store.serviceCursor(receipt);
334
337
  for (const [index, part] of parts.entries()) {
335
338
  if (index < delivered)
package/dist/telegram.js CHANGED
@@ -920,7 +920,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
920
920
  };
921
921
  /** The lead's words (lead-voice.ts): one message; a later say within two minutes edits it in place. */
922
922
  const leadSayOutcome = async (fact) => {
923
- const words = leadSayText(store, fact);
923
+ const words = leadSayText(store, fact, binding.approver);
924
924
  const button = fact.link === null ? null : factButton(phoneOrigin, fact.link);
925
925
  const keyboard = button === null ? [] : [button];
926
926
  const earlier = leadSayEarlier(store, fact).map(id => store.telegramMessageOf(id, fact.destination)).find(one => one !== null) ?? null;
package/dist/test-temp.js CHANGED
@@ -18,7 +18,7 @@ export const TEST_TEMP_PREFIXES = [
18
18
  "flow-goal-", "flow-scratch", "flows-chat-", "invoke-attest-", "knowledge-test-", "learning-", "peek-", "phone-origin-",
19
19
  "proposal-repo-", "race-", "refresh-migration-", "refresh-refusal-", "release-check-", "shared-chat-", "skills-test-",
20
20
  "task-status-surfaces-", "template-repo-", "template-test-", "v14-migrate-", "v17-migrate-", "watch-anchor-",
21
- "needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-", "lead-promises-",
21
+ "needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-", "lead-promises-", "lead-people-",
22
22
  // Playwright's browser profiles and artifacts.
23
23
  "playwright_chromiumdev_profile-", "playwright_firefoxdev_profile-", "playwright-artifacts-",
24
24
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.16",
3
+ "version": "0.9.17",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"