@llblab/pi-kit 0.36.3 → 0.37.1

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 (40) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
  4. package/node_modules/@llblab/pi-telegram/BACKLOG.md +9 -11
  5. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +19 -0
  6. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
  7. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
  8. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +5 -6
  9. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +36 -35
  10. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +2 -3
  11. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
  12. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
  13. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
  14. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
  15. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
  16. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
  17. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
  18. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
  19. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
  20. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +29 -20
  21. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  22. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
  23. package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -6
  24. package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
  25. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  26. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  27. package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
  28. package/node_modules/@llblab/pi-telegram/docs/public-api.md +5 -3
  29. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +4 -0
  30. package/node_modules/@llblab/pi-telegram/lib/commands.ts +53 -43
  31. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -3
  32. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
  33. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
  34. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
  35. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
  36. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
  37. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +31 -18
  38. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  39. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
  40. package/package.json +2 -2
@@ -34,7 +34,7 @@ export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
34
34
  "For an explicitly requested channel post, pass its exact numeric id or public @username as chat_id; no local channel registry is required, and Telegram remains the authority on the bot's posting permission.",
35
35
  "For an explicitly requested channel media post, pass one local .jpg/.jpeg/.png/.webp photo or .mp4 video as media; the text becomes its caption (max 1024 characters), and albums or other media types are rejected.",
36
36
  "For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.",
37
- "Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
37
+ "Add buttons by embedding the same telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
38
38
  "During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread.",
39
39
  ] as const;
40
40
 
@@ -360,7 +360,7 @@ export interface TelegramTypingLoopStarterDeps<
360
360
  getDefaultChatId: () => number | undefined;
361
361
  sendTypingAction: (
362
362
  chatId: number,
363
- options?: { message_thread_id?: number; assertAuthority?: () => void },
363
+ options?: { message_thread_id?: number; assertAuthority?: () => void; isCancelled?: () => boolean },
364
364
  ) => Promise<unknown>;
365
365
  updateStatus: (ctx: TContext, error?: string) => void;
366
366
  isContextActive?: (ctx: TContext) => boolean;
@@ -454,6 +454,7 @@ export function createTelegramTypingLoopStarter<TContext>(
454
454
  try {
455
455
  await deps.sendTypingAction(targetChatId, {
456
456
  ...actionOptions,
457
+ isCancelled: () => !active,
457
458
  // This stays process-local; adapters recheck it before HTTP/IPC issuance, not just loop ticks.
458
459
  assertAuthority() {
459
460
  if (!active || !canContinue()) throw new Error("Typing context authority changed.");
@@ -407,8 +407,8 @@ export interface TelegramApiCallOptions {
407
407
 
408
408
  export class TelegramApiAuthorityError extends Error {
409
409
  readonly requestIssued: boolean;
410
- constructor(requestIssued: boolean) {
411
- super("Telegram API call authority is unavailable.");
410
+ constructor(requestIssued: boolean, options?: ErrorOptions) {
411
+ super("Telegram API call authority is unavailable.", options);
412
412
  this.name = "TelegramApiAuthorityError";
413
413
  this.requestIssued = requestIssued;
414
414
  }
@@ -418,11 +418,12 @@ export class TelegramApiAuthorityError extends Error {
418
418
  export function assertTelegramApiCallAuthority(
419
419
  assertAuthority: (() => void) | undefined,
420
420
  requestIssued: boolean,
421
+ errorOptions?: ErrorOptions,
421
422
  ): void {
422
423
  try {
423
424
  assertAuthority?.();
424
425
  } catch {
425
- throw new TelegramApiAuthorityError(requestIssued);
426
+ throw new TelegramApiAuthorityError(requestIssued, errorOptions);
426
427
  }
427
428
  }
428
429
 
@@ -847,6 +848,8 @@ export interface TelegramBridgeApiRuntimeDeps {
847
848
  /** Typing issuance guard is local authority, never Bot API body or IPC data. */
848
849
  export interface TelegramTypingActionOptions extends Pick<TelegramApiCallOptions, "assertAuthority"> {
849
850
  message_thread_id?: number;
851
+ /** Diagnostic observation only: an ended loop does not grant issuance, success or retry. */
852
+ isCancelled?: () => boolean;
850
853
  }
851
854
 
852
855
  export interface TelegramBridgeApiRuntime extends Pick<
@@ -1649,7 +1652,7 @@ async function callTelegramWithRetry<TResponse>(
1649
1652
  } catch (error) {
1650
1653
  // Authority refusal is never a transport failure, retry grant or acknowledged success.
1651
1654
  if (error instanceof TelegramApiAuthorityError) throw error;
1652
- assertTelegramApiCallAuthority(assertAuthority, requestIssued);
1655
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued, { cause: error });
1653
1656
  const retryable =
1654
1657
  isRetryableTelegramApiError(error) &&
1655
1658
  !(
@@ -2090,7 +2093,8 @@ export function createTelegramBridgeApiRuntime(
2090
2093
  notBeforeMs: number;
2091
2094
  };
2092
2095
  const chatActionGates = new Map<string, ChatActionGate>();
2093
- const chatActionChatGates = new Map<string, ChatActionGate>();
2096
+ // Pin shared cooldowns until every concurrent Thread settles; this state never owns their sends.
2097
+ const chatActionChatGates = new Map<string, { pendingRequests: number; notBeforeMs: number }>();
2094
2098
  const getChatActionKeys = (
2095
2099
  method: string,
2096
2100
  body: Record<string, unknown>,
@@ -2130,7 +2134,7 @@ export function createTelegramBridgeApiRuntime(
2130
2134
  }
2131
2135
  }
2132
2136
  for (const [key, candidate] of chatActionChatGates) {
2133
- if (!candidate.inFlight && nowMs >= candidate.notBeforeMs) {
2137
+ if (candidate.pendingRequests === 0 && nowMs >= candidate.notBeforeMs) {
2134
2138
  chatActionChatGates.delete(key);
2135
2139
  }
2136
2140
  }
@@ -2146,11 +2150,10 @@ export function createTelegramBridgeApiRuntime(
2146
2150
  if (chatActionChatGates.size >= chatActionMaxGates) {
2147
2151
  return true as TResponse;
2148
2152
  }
2149
- chatGate = { notBeforeMs: 0 };
2153
+ chatGate = { pendingRequests: 0, notBeforeMs: 0 };
2150
2154
  chatActionChatGates.set(chatActionKeys.chat, chatGate);
2151
2155
  }
2152
2156
  if (gate.inFlight) return (await gate.inFlight) as TResponse;
2153
- if (chatGate.inFlight) return true as TResponse;
2154
2157
  if (nowMs < gate.notBeforeMs || nowMs < chatGate.notBeforeMs) {
2155
2158
  return true as TResponse;
2156
2159
  }
@@ -2189,19 +2192,22 @@ export function createTelegramBridgeApiRuntime(
2189
2192
  );
2190
2193
  return true as TResponse;
2191
2194
  }
2192
- deps.recordRuntimeEvent(
2193
- "api",
2194
- error,
2195
- withTelegramTransportDiagnostics(error, { method }),
2196
- );
2195
+ const diagnosticError = error instanceof TelegramApiAuthorityError && Object.hasOwn(error, "cause")
2196
+ ? error.cause : error;
2197
+ if (!options?.isExpectedError?.(error))
2198
+ deps.recordRuntimeEvent(
2199
+ "api",
2200
+ diagnosticError,
2201
+ withTelegramTransportDiagnostics(diagnosticError, { method }),
2202
+ );
2197
2203
  throw error;
2198
2204
  })
2199
2205
  .finally(() => {
2200
2206
  if (gate.inFlight === request) gate.inFlight = undefined;
2201
- if (chatGate.inFlight === request) chatGate.inFlight = undefined;
2207
+ chatGate.pendingRequests--;
2202
2208
  });
2203
2209
  gate.inFlight = request;
2204
- chatGate.inFlight = request;
2210
+ chatGate.pendingRequests++;
2205
2211
  return request;
2206
2212
  }
2207
2213
  try {
@@ -2311,14 +2317,21 @@ export function createTelegramBridgeApiRuntime(
2311
2317
  : {}),
2312
2318
  }),
2313
2319
  sendTypingAction: createTelegramChatActionSender(
2314
- (chatId, action, options) =>
2315
- callRecorded<boolean>("sendChatAction", {
2320
+ (chatId, action, options) => {
2321
+ const isCancelled = options?.isCancelled;
2322
+ return callRecorded<boolean>("sendChatAction", {
2316
2323
  chat_id: chatId,
2317
2324
  action,
2318
2325
  ...(options?.message_thread_id !== undefined
2319
2326
  ? { message_thread_id: options.message_thread_id }
2320
2327
  : {}),
2321
- }, { assertAuthority: options?.assertAuthority }),
2328
+ }, {
2329
+ assertAuthority: options?.assertAuthority,
2330
+ ...(isCancelled ? {
2331
+ isExpectedError: (error: unknown) => error instanceof TelegramApiAuthorityError && !Object.hasOwn(error, "cause") && isCancelled() === true,
2332
+ } : {}),
2333
+ });
2334
+ },
2322
2335
  "typing",
2323
2336
  ),
2324
2337
  sendRecordVoiceAction: createTelegramChatActionSender(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.57.11",
3
+ "version": "0.58.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,12 +16,12 @@ Use Telegram as a mobile companion to the current Pi session. Preserve the exact
16
16
  | Explicitly send from local/TUI to Telegram | `telegram_message` or `telegram_attach` |
17
17
  | Explicitly send to a different live Thread | `telegram_message(thread=...)` |
18
18
  | Add buttons | Hidden comment for footer; `telegram_button` fence for in-body rows |
19
- | Add explicit voice | Top-level hidden action comment |
19
+ | Add explicit voice | Hidden action comment |
20
20
  | Build a repeated deterministic interaction | Follow `generative-apps` |
21
21
 
22
22
  A connected Telegram session proves capability, not user intent. Use Telegram features on Telegram-originated turns or explicit Telegram delivery requests only. Never call `telegram_message` for the current active target.
23
23
 
24
- For direct delivery, Thread routing, configuration, or diagnosis, read only the applicable reference listed under [Conditional References](#conditional-references).
24
+ For delivery, Thread routing, configuration or diagnosis, read the applicable reference under [Conditional References](#conditional-references).
25
25
 
26
26
  ## Turn Context
27
27
 
@@ -40,7 +40,7 @@ Reply in concise, phone-width Telegram Rich Markdown. Use `$...$` and `$$...$$`
40
40
 
41
41
  ## Assistant Actions
42
42
 
43
- `telegram_button` and `telegram_voice` are markup, not tools. Emit action comments at column zero outside lists, quotes, code, and indentation. Comments create footer buttons or voice artifacts. For buttons between paragraphs, use a column-zero triple-backtick `telegram_button` block. Both button wrappers accept the same singleton JSON/CML cell or mixed matrix; the wrapper determines placement. Telegram removes every assistant-authored HTML comment from previews and final replies regardless of owner or Markdown position; only recognized top-level wrappers activate actions; comment-only output sends no text message.
43
+ `telegram_button` and `telegram_voice` are markup, not tools. Complete comments activate regardless of surrounding Markdown. Escape `<!--` as `&lt;!--` for literal examples; backticks and backslashes do not disable actions. Comments create footer buttons or voice artifacts. In-body fences: ≥3 backticks/tildes, optional indentation/whitespace, exact `telegram_button`; separate same-marker closure, no shorter. Both wrappers accept a singleton JSON/CML cell or mixed matrix; the wrapper determines placement. Telegram removes every assistant-authored HTML comment from previews and final replies regardless of owner or Markdown position; only complete recognized comments or valid button fences activate actions; comment-only output sends no text message.
44
44
 
45
45
  ### Shared Encoding Rule
46
46
 
@@ -61,7 +61,7 @@ Every enabled button has a self-contained prompt and an optional selection style
61
61
  - `{label|prompt|selected_style}` and `{|prompt|selected_style}` accept `primary`, `success`, or `danger`.
62
62
  - Fourth-position `1`/`true` disables, `0`/`false` enables; omission means enabled. JSON uses boolean `disabled`. `{|Next||1}` omits label/style; `{Next|||1}` omits prompt/style; `{|||1}` is blank (Telegram receives a non-breaking space). Prefer meaningful labels and retain a useful enabled action. Enabled CML requires a prompt. Disabled controls stay visible but have no callback, queued prompt, or bound-method invocation.
63
63
  - Both wrappers concatenate rows from adjacent JSON/CML cells or matrices, with optional commas. Cells are vertical; horizontal peers require a nested row: `[[{A|a}{B|b}]]`.
64
- - Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Outer code fences and quoted/indented examples remain literal.
64
+ - Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Enclosing code fences and quoted/list-prefixed button fences remain literal.
65
65
  - Both placements share prompt/app routing. In-body clicks acknowledge without recoloring; selected-style highlighting remains footer-only.
66
66
 
67
67
  A single in-body button (the four-backtick wrapper below makes this a literal example):
@@ -124,6 +124,6 @@ Before replying:
124
124
 
125
125
  - Use the ordinary path for the current target and direct tools only for explicit other delivery.
126
126
  - Attach requested files rather than only mentioning them.
127
- - Keep action comments and button fences top-level, complete, and canonical: CML first, JSON when necessary; either syntax may coexist within one matrix.
127
+ - Keep comments complete and button fences top-level and canonical: CML first, JSON when necessary; either syntax may coexist within one matrix.
128
128
  - Give every enabled button a self-contained prompt; preserve confirmation for dangerous actions.
129
129
  - Expose no secret or hidden reasoning.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.36.3",
3
+ "version": "0.37.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -50,7 +50,7 @@
50
50
  "@llblab/pi-codex-usage": "0.13.2",
51
51
  "@llblab/pi-grow-loop": "0.9.0",
52
52
  "@llblab/pi-state-flow": "0.26.5",
53
- "@llblab/pi-telegram": "0.57.11",
53
+ "@llblab/pi-telegram": "0.58.3",
54
54
  "@llblab/skills": "1.15.0"
55
55
  },
56
56
  "bundledDependencies": [