@llblab/pi-kit 0.10.6 → 0.10.7

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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.10.7 - 2026-09-12
6
+
7
+ - `Queue Enqueue`: Advances the exact Telegram pin to `0.45.7`, preventing asynchronous voice/file preparation from restoring consumed prompts or overwriting newer queue changes. Surviving history and receipts follow current queue state; an already-wedged in-memory queue still requires separately authorized recovery. The package set, resource inventory, and load order remain unchanged.
8
+
5
9
  ## 0.10.6 - 2026-09-12
6
10
 
7
11
  - `Guest Placeholder And Delivery`: Advances the exact Telegram pin to `0.45.6`, so the guest ACK rotates the globe placeholder in whole cycles and holds `🌏 Working on it...`, guest answers are still delivered when a companion extension suppresses the final assistant message, `429` retry waits are recorded as `api` runtime events, the Thread display settings card drops the manual `/name` hint, and empty-queue headings carry no trailing period. The package set, resource inventory, and explicit load order remain unchanged.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
15
15
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.9.4` | Compact Codex/Spark subscription-limit status |
16
16
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.1` | Visible continuation scheduling and bounded worker Skills |
17
17
  | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.9.6` | Atomic scoped state cohorts with final-eligibility resolution |
18
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.6` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
18
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.7` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
19
19
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
20
20
 
21
21
  Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
@@ -2,6 +2,7 @@
2
2
 
3
3
  _This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
4
4
 
5
+ - [ ] `Prompt enqueue hotfix` (`optional`, `operator-gated`): Optional nonblocking operator-authorized disposable-follower smoke: overlap delayed voice processing with turn completion and confirm one-time ordered consumption and truthful counts against the [queue contract](./docs/architecture.md#queue-and-dispatch-safety). Separately authorized supported recovery investigation remains open: prevention does not repair an already-wedged in-memory queue; establish the exact recovery path and preservation/discard consequences before mutation, otherwise report the blocker. No journal/ownership edits, replay of settled input, implicit queue clearing, or restart; live activation requires separate operator authorization.
5
6
  - [ ] `Channel multimedia posts` (`0.45.1`, live-acceptance-gated): `telegram_message` channel delivery accepts one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video, uploads it through the multipart transport as `sendPhoto`/`sendVideo` with `text` as the HTML caption, validates kind and size (photo ≤ 10 MiB, video ≤ 50 MiB) plus ≤ 1024 visible caption characters before issuance, and rejects unsupported types and albums instead of downgrading them to links. The channel-post journal binds kind/file name/byte size/SHA-256 and caption, so duplicate requests and lost acknowledgements never re-upload; media-post edits replace the caption through `editMessageCaption`, and Markdown spoilers render as `<tg-spoiler>`. Live image publication passed on `@llb_log`. Regressions cover confirmed publication, duplicate requests, lost ACK, pre-issuance rejection, caption edits, and reconnect replacement. Remaining: operator-authorized disposable-channel acceptance of rejected upload, duplicate request, and caption edit.
6
7
  - [ ] `Manual Thread naming` (`gated-but-preparable`, release priority): Local bot-owned `/name Name` and bare `/name` flows avoid model dispatch. One expiring exact-target input dialog immediately accepts the next valid name, always offers cancel, and offers **Reset to automatic** only while a manual override exists; duplicate/stale callbacks cannot repeat mutation. Durable manual override supersedes every automatic display mode, reset is leader/follower generation- and target-fenced, and Letters remains the default without rewriting recovery identity. Local review findings are remediated, including Bot-API-wait target-replacement regressions for leader/follower rename and reset. Remaining: disposable live acceptance for command-menu ordering, dialog, invalid input, duplicate callbacks, leader/follower rename and reset.
7
8
  - [ ] `OMP schema acceptance` ([#267](https://github.com/llblab/pi-telegram/issues/267), `human-/environment-gated`): Local emitted-schema, Pi process, and llama.cpp source checks now prove explicit recursive JSON values, root `$defs`, only supported local `#/...` references, and no bare boolean schema. Confirm one connected `telegram_bind` request through the reporter's OMP + llama-server build before closing interoperability acceptance; do not treat this progressive external check as a Pi release blocker.
@@ -4,6 +4,10 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.45.7: Queue Enqueue Race Hotfix
8
+
9
+ - `Queue Enqueue`: Asynchronous voice/file preparation no longer restores consumed prompts or overwrites newer queue changes, preventing a settled phantom head from blocking accepted work. Final assembly uses current queue state and allocates order at commit; abort-history folding retains only surviving intended prompts and receipts while preserving a handed-off head until `agent_start`.
10
+
7
11
  ## 0.45.6: Guest Placeholder And Delivery Hotfix
8
12
 
9
13
  - `Guest Placeholder`: The guest ACK answers with the first bold globe frame (`🌎 Working on it.`) and rotates `🌍`/`🌏` once per second with the dots growing every two seconds. Rotation now completes whole 6-frame cycles and stops only after the 20 s minimum, so a pending answer holds the cycle's final frame (`🌏 Working on it...`) instead of cutting mid-step; a 26 s safety bound stays clear of the ~28 s Telegram flood-control wall, and error backoff and pre-replacement cancellation are unchanged.
@@ -480,6 +480,8 @@ Menu and reaction controls share the same canonical queue state. A menu Keep can
480
480
 
481
481
  Admission and planning validate lane contracts. Invalid lane/kind pairings fail predictably instead of being silently coerced.
482
482
 
483
+ Prompt preparation completes downloads, inbound handlers, and binary image reads before synchronous final assembly and queue commit. Enqueue captures only intended abort-history identities before preparation; afterward it reads the current queue, folds surviving selected prompts using their current text and receipts, allocates the new turn's order, and appends without another asynchronous boundary. The handed-off head remains in place until `agent_start` consumes it, even if preparation finishes first. Concurrent arrivals, removals, and lane/reaction edits remain intact; preparation failure or stale-generation completion leaves intervening queue changes untouched.
484
+
483
485
  Dispatch requires:
484
486
 
485
487
  - No active Telegram turn.
@@ -2349,16 +2349,18 @@ export interface TelegramControlQueueController<TContext> {
2349
2349
  ) => void;
2350
2350
  }
2351
2351
 
2352
+ export type TelegramPreparedPromptTurn = (
2353
+ historyTurns: PendingTelegramTurn[],
2354
+ ) => PendingTelegramTurn;
2355
+
2352
2356
  export interface TelegramPromptEnqueueRuntimeDeps<
2353
2357
  TMessage,
2354
2358
  TContext = unknown,
2355
2359
  > extends TelegramQueueStore<TContext> {
2360
+ hasPendingDispatch: () => boolean;
2356
2361
  getFoldQueuedPromptsIntoHistory: () => boolean;
2357
2362
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
2358
- createTurn: (
2359
- messages: TMessage[],
2360
- historyTurns: PendingTelegramTurn[],
2361
- ) => Promise<PendingTelegramTurn>;
2363
+ prepareTurn: (messages: TMessage[]) => Promise<TelegramPreparedPromptTurn>;
2362
2364
  updateStatus: () => void;
2363
2365
  dispatchNextQueuedTelegramTurn: () => void;
2364
2366
  assertExecutionCurrent?: () => void;
@@ -2369,13 +2371,13 @@ export interface TelegramPromptEnqueueControllerDeps<
2369
2371
  TMessage,
2370
2372
  TContext = unknown,
2371
2373
  > extends TelegramQueueStore<TContext> {
2374
+ hasPendingDispatch: () => boolean;
2372
2375
  getFoldQueuedPromptsIntoHistory: () => boolean;
2373
2376
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
2374
- createTurn: (
2377
+ prepareTurn: (
2375
2378
  messages: TMessage[],
2376
- historyTurns: PendingTelegramTurn[],
2377
2379
  ctx: TContext,
2378
- ) => Promise<PendingTelegramTurn>;
2380
+ ) => Promise<TelegramPreparedPromptTurn>;
2379
2381
  updateStatus: (ctx: TContext) => void;
2380
2382
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
2381
2383
  assertExecutionCurrent?: (messages: TMessage[]) => void;
@@ -2727,17 +2729,25 @@ export async function enqueueTelegramPromptTurnRuntime<
2727
2729
  messages: TMessage[],
2728
2730
  deps: TelegramPromptEnqueueRuntimeDeps<TMessage, TContext>,
2729
2731
  ): Promise<PendingTelegramTurn> {
2730
- const enqueuePlan = planTelegramPromptEnqueue(
2732
+ deps.assertExecutionCurrent?.();
2733
+ const historyOrders = new Set(planTelegramPromptEnqueue(
2731
2734
  deps.getQueuedItems(),
2732
2735
  deps.getFoldQueuedPromptsIntoHistory(),
2733
- );
2734
- deps.assertExecutionCurrent?.();
2736
+ ).historyTurns.map((turn) => turn.queueOrder));
2735
2737
  deps.setFoldQueuedPromptsIntoHistory(false);
2736
- const turn = await deps.createTurn(messages, enqueuePlan.historyTurns);
2738
+ const buildTurn = await deps.prepareTurn(messages);
2737
2739
  deps.assertExecutionCurrent?.();
2738
- deps.setQueuedItems(
2739
- appendTelegramQueueItem(enqueuePlan.remainingItems, turn),
2740
- );
2740
+ // Preserve the Pi-owned head until agent_start, plus later arrivals and current edits/reactions.
2741
+ const pendingDispatch = deps.hasPendingDispatch();
2742
+ const historyTurns: PendingTelegramTurn[] = [];
2743
+ const remainingItems = deps.getQueuedItems().filter((item, index) => {
2744
+ if ((pendingDispatch && index === 0) || !isPendingTelegramTurn(item) ||
2745
+ !historyOrders.has(item.queueOrder)) return true;
2746
+ historyTurns.push(item);
2747
+ return false;
2748
+ });
2749
+ const turn = buildTurn(historyTurns);
2750
+ deps.setQueuedItems(appendTelegramQueueItem(remainingItems, turn));
2741
2751
  deps.onQueued?.(turn);
2742
2752
  deps.updateStatus();
2743
2753
  deps.dispatchNextQueuedTelegramTurn();
@@ -2754,8 +2764,7 @@ export function createTelegramPromptEnqueueController<
2754
2764
  enqueue: (messages, ctx, onQueued) =>
2755
2765
  enqueueTelegramPromptTurnRuntime(messages, {
2756
2766
  ...deps,
2757
- createTurn: (nextMessages, historyTurns) =>
2758
- deps.createTurn(nextMessages, historyTurns, ctx),
2767
+ prepareTurn: (nextMessages) => deps.prepareTurn(nextMessages, ctx),
2759
2768
  updateStatus: () => deps.updateStatus(ctx),
2760
2769
  dispatchNextQueuedTelegramTurn: () =>
2761
2770
  deps.dispatchNextQueuedTelegramTurn(ctx),
@@ -2095,7 +2095,7 @@ export function createTelegramInboundRouteRuntime<
2095
2095
  }
2096
2096
  await menuCallbackHandler(query, ctx);
2097
2097
  };
2098
- const promptTurnBuilder = Turns.createTelegramPromptTurnRuntimeBuilder<
2098
+ const preparePromptTurn = Turns.createTelegramPromptTurnRuntimePreparer<
2099
2099
  TMessage,
2100
2100
  TContext
2101
2101
  >({
@@ -2124,7 +2124,8 @@ export function createTelegramInboundRouteRuntime<
2124
2124
  text: "continue",
2125
2125
  caption: undefined,
2126
2126
  } as TMessage;
2127
- const turn = await promptTurnBuilder([continueMessage], [], ctx);
2127
+ const buildTurn = await preparePromptTurn([continueMessage], ctx);
2128
+ const turn = buildTurn([]);
2128
2129
  const continueTurn = {
2129
2130
  ...turn,
2130
2131
  queueLane: "control" as const,
@@ -2262,15 +2263,19 @@ export function createTelegramInboundRouteRuntime<
2262
2263
  const promptEnqueueController =
2263
2264
  Queue.createTelegramPromptEnqueueController<TMessage, TContext>({
2264
2265
  ...deps.telegramQueueStore,
2266
+ hasPendingDispatch: deps.bridgeRuntime.lifecycle.hasDispatchPending,
2265
2267
  getFoldQueuedPromptsIntoHistory:
2266
2268
  deps.bridgeRuntime.lifecycle.shouldFoldQueuedPromptsIntoHistory,
2267
2269
  setFoldQueuedPromptsIntoHistory:
2268
2270
  deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory,
2269
- createTurn: async (messages, historyTurns, turnCtx) => {
2270
- const turn = await promptTurnBuilder(messages, historyTurns, turnCtx);
2271
- return turn.replyToMessageId > 0
2272
- ? turn
2273
- : { ...turn, replyToMessageId: 0 };
2271
+ prepareTurn: async (messages, turnCtx) => {
2272
+ const buildTurn = await preparePromptTurn(messages, turnCtx);
2273
+ return (historyTurns) => {
2274
+ const turn = buildTurn(historyTurns);
2275
+ return turn.replyToMessageId > 0
2276
+ ? turn
2277
+ : { ...turn, replyToMessageId: 0 };
2278
+ };
2274
2279
  },
2275
2280
  updateStatus: deps.updateStatus,
2276
2281
  dispatchNextQueuedTelegramTurn: requestDispatchNextQueuedTelegramTurn,
@@ -26,6 +26,7 @@ import {
26
26
  createTelegramQueueAdmissionReceipt,
27
27
  truncateTelegramQueueSummary,
28
28
  type PendingTelegramTurn,
29
+ type TelegramPreparedPromptTurn,
29
30
  type TelegramPromptContent,
30
31
  type TelegramQueueAdmissionReceipt,
31
32
  type TelegramQueueItem,
@@ -454,17 +455,16 @@ export interface TelegramPromptTurnRuntimeBuilderDeps<
454
455
  assertExecutionCurrent?: (message: TelegramTurnMessage) => void;
455
456
  }
456
457
 
457
- export function createTelegramPromptTurnRuntimeBuilder<
458
+ export function createTelegramPromptTurnRuntimePreparer<
458
459
  TMessage extends TelegramTurnMessage & TelegramMediaMessage,
459
460
  TContext = unknown,
460
461
  >(
461
462
  deps: TelegramPromptTurnRuntimeBuilderDeps<TContext>,
462
463
  ): (
463
464
  messages: TMessage[],
464
- historyTurns?: PendingTelegramTurn[],
465
465
  ctx?: TContext,
466
- ) => Promise<PendingTelegramTurn> {
467
- return async (messages, historyTurns = [], ctx) => {
466
+ ) => Promise<TelegramPreparedPromptTurn> {
467
+ return async (messages, ctx) => {
468
468
  const rawText = extractTelegramMessagesText(messages);
469
469
  const firstMessage = messages[0];
470
470
  if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
@@ -577,11 +577,10 @@ export function createTelegramPromptTurnRuntimeBuilder<
577
577
  thread: threadLabel,
578
578
  "from-thread": firstMessage?.pi_telegram_agent_source_thread,
579
579
  });
580
- return buildTelegramPromptTurnRuntime({
580
+ const buildTurn = await prepareTelegramPromptTurn({
581
581
  telegramPrefix,
582
582
  messages,
583
- historyTurns,
584
- queueOrder: deps.allocateQueueOrder(),
583
+ readBinaryFile: readFile,
585
584
  rawText: promptRawText,
586
585
  sourceContext,
587
586
  statusText: processed.rawText,
@@ -602,6 +601,10 @@ export function createTelegramPromptTurnRuntimeBuilder<
602
601
  admissionScope: deps.getAdmissionScope?.(),
603
602
  admissionJournalBinding: deps.getAdmissionJournalBinding?.(),
604
603
  });
604
+ return (historyTurns) => {
605
+ if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
606
+ return buildTurn(deps.allocateQueueOrder(), historyTurns);
607
+ };
605
608
  };
606
609
  }
607
610
 
@@ -670,9 +673,29 @@ function collectTelegramTurnAdmissionReceipts(
670
673
  return [...receipts.values()];
671
674
  }
672
675
 
673
- export async function buildTelegramPromptTurn(
676
+ async function prepareTelegramPromptTurn(
677
+ options: Omit<BuildTelegramPromptTurnOptions, "queueOrder" | "historyTurns">,
678
+ ): Promise<(queueOrder: number, historyTurns: PendingTelegramTurn[]) => PendingTelegramTurn> {
679
+ const images: TelegramPromptContent[] = [];
680
+ for (const file of options.files) {
681
+ if (!file.isImage) continue;
682
+ const mediaType = file.mimeType || options.inferImageMimeType(file.path);
683
+ if (!mediaType) continue;
684
+ const buffer = await options.readBinaryFile(file.path);
685
+ images.push({
686
+ type: "image",
687
+ data: Buffer.from(buffer).toString("base64"),
688
+ mimeType: mediaType,
689
+ });
690
+ }
691
+ return (queueOrder, historyTurns) =>
692
+ buildPreparedTelegramPromptTurn({ ...options, queueOrder, historyTurns }, images);
693
+ }
694
+
695
+ function buildPreparedTelegramPromptTurn(
674
696
  options: BuildTelegramPromptTurnOptions,
675
- ): Promise<PendingTelegramTurn> {
697
+ images: TelegramPromptContent[],
698
+ ): PendingTelegramTurn {
676
699
  const firstMessage = options.messages[0];
677
700
  if (!firstMessage) {
678
701
  throw new Error("Missing Telegram message for turn creation");
@@ -703,18 +726,8 @@ export async function buildTelegramPromptTurn(
703
726
  ),
704
727
  }),
705
728
  },
729
+ ...images,
706
730
  ];
707
- for (const file of options.files) {
708
- if (!file.isImage) continue;
709
- const mediaType = file.mimeType || options.inferImageMimeType(file.path);
710
- if (!mediaType) continue;
711
- const buffer = await options.readBinaryFile(file.path);
712
- content.push({
713
- type: "image",
714
- data: Buffer.from(buffer).toString("base64"),
715
- mimeType: mediaType,
716
- });
717
- }
718
731
  if (options.voicePromptContribution?.trim()) {
719
732
  const textItem = content.find((c) => c.type === "text") as
720
733
  { type: "text"; text: string } | undefined;
@@ -759,6 +772,13 @@ export async function buildTelegramPromptTurn(
759
772
  };
760
773
  }
761
774
 
775
+ export async function buildTelegramPromptTurn(
776
+ options: BuildTelegramPromptTurnOptions,
777
+ ): Promise<PendingTelegramTurn> {
778
+ const buildTurn = await prepareTelegramPromptTurn(options);
779
+ return buildTurn(options.queueOrder, options.historyTurns ?? []);
780
+ }
781
+
762
782
  export async function buildTelegramPromptTurnRuntime(
763
783
  options: BuildTelegramPromptTurnRuntimeOptions,
764
784
  ): Promise<PendingTelegramTurn> {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.45.6",
3
+ "version": "0.45.7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -45,7 +45,7 @@
45
45
  "@llblab/pi-codex-usage": "0.9.4",
46
46
  "@llblab/pi-grow-loop": "0.8.1",
47
47
  "@llblab/pi-state-flow": "0.9.6",
48
- "@llblab/pi-telegram": "0.45.6",
48
+ "@llblab/pi-telegram": "0.45.7",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },
51
51
  "bundledDependencies": [