@llblab/pi-kit 0.17.1 → 0.18.0

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,12 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.18.0 - 2026-09-18
6
+
7
+ - `Telegram Lifecycle Gateway`: Advances the exact Telegram pin to `0.50.0`, replacing the session-specific technical command with one guarded internal gateway for runtime-armed lifecycle actions while preserving `/new` settlement, continuity, and terminal-result guarantees.
8
+ - `Thinking Cadence`: Buffers the first Telegram thinking frame for two seconds and throttles later updates to the same two-second cadence as answer drafts, while still flushing remaining reasoning when the block completes.
9
+ - `Package Cohort`: Keeps every other bundled package at its current exact version; the package set, resource inventory, and explicit load order remain unchanged.
10
+
5
11
  ## 0.17.1 - 2026-09-18
6
12
 
7
13
  - `State Flow Activation Hotfixes`: Advances the exact State Flow pin to `0.16.2`, allowing resumed activation to reassert its selected session cohort and allowing passive global memory before a new CWD has materialized, while retaining fail-closed handling for genuinely incomplete ownership.
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.10.0` | Compact Codex/Spark subscription-limit and Business credit-usage 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.16.2` | Incremental scoped state/context/memory compiler with proportional operational guidance, intentional agency, reactive dangling-reference diagnostics, compiled runtime delivery, safe compaction, and exact publication |
18
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.49.0` | Telegram companion with native fresh-session replacement, adaptive Thread continuity, exact queues, files, voice, controls, and Generative Apps guidance |
18
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.50.0` | Telegram companion with native fresh-session replacement, a guarded internal lifecycle gateway, buffered thinking cadence, adaptive Thread continuity, exact 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.
@@ -121,7 +121,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
121
121
  - Command templates remain compact and shell-free. Use string leaves or ordered `template` arrays; shell operators are not an execution contract. Examples use portable executable placeholders, never machine-local paths.
122
122
  - `telegram_attach` is the canonical file path and `telegram_message` the direct Markdown text/buttons path. Both require current direct or registered-follower authority and must not replace the normal active-turn reply.
123
123
  - Inbound handlers transform text/media before queueing; outbound handlers precede programmatic/provider fallbacks. Public contracts and ordering live in `docs/inbound.md`, `docs/outbound.md`, and `docs/public-api.md`.
124
- - Pi integration uses public hooks and APIs. Telegram `/new` is scheduled against the exact durable update, dispatched only after that update is removed from the journal, and then routed through an internal Pi command via `pi.sendUserMessage(..., { expandPromptTemplates: true })`; the command handler receives the real `ExtensionCommandContext` and calls `ctx.newSession()`. Before replacement, CAS-publish one exact expiring handoff in the profile target snapshot. `workspace-thread` successors re-key the matching Workspace binding; `classic-chat` successors preserve Profile/CWD/session/chat continuity without creating a binding or invoking topic APIs. Both atomically claim the intent before one terminal result, and the old `withSession` path never publishes the same success. Never replace the session while inbound authority is unsettled, store stale command contexts, accept an expired or mismatched handoff, inject terminal input, spawn a shadow Pi process, or mutate session files.
124
+ - Pi integration uses public hooks and APIs. Telegram `/new` is scheduled against the exact durable update, dispatched only after that update is removed from the journal, and then routed through the single `/telegram-internal` Pi gateway via `pi.sendUserMessage(..., { expandPromptTemplates: true })`; only a runtime-armed typed action may execute, manual invocation reports that the command cannot be run manually, and the handler receives the real `ExtensionCommandContext` before calling `ctx.newSession()`. Before replacement, CAS-publish one exact expiring handoff in the profile target snapshot. `workspace-thread` successors re-key the matching Workspace binding; `classic-chat` successors preserve Profile/CWD/session/chat continuity without creating a binding or invoking topic APIs. Both atomically claim the intent before one terminal result, and the old `withSession` path never publishes the same success. Never replace the session while inbound authority is unsettled, store stale command contexts, accept an expired or mismatched handoff, inject terminal input, spawn a shadow Pi process, or mutate session files.
125
125
 
126
126
  ## 7. Engineering Conventions
127
127
 
@@ -4,6 +4,11 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.50.0: Internal lifecycle gateway and thinking cadence
8
+
9
+ - `Internal lifecycle gateway`: Replaces the session-specific technical command with one `/telegram-internal` gateway for runtime-armed typed actions. Manual invocation performs no action and explains that the command cannot be run manually; settled `/new` replacement retains the same lifecycle and terminal-result guarantees.
10
+ - `Thinking cadence`: Thinking now follows the same cadence as answer drafts: it accumulates the opening frame for two seconds, then updates at most once every two seconds while still flushing buffered reasoning when the block completes.
11
+
7
12
  ## 0.49.0: Unified fresh-session continuity
8
13
 
9
14
  - `/new`: Classic and Threaded Mode now share one confirmed fresh-session flow. The settled callback deletes its dialog and publishes an expiring exact-target intent; a same- or cross-process successor preserves the classic chat or re-keys the Thread/slot/name, atomically claims once, then sends one terminal result. Identity mismatches fail closed.
@@ -126,7 +126,7 @@ Enable the optional capabilities the bridge needs in the [@BotFather](https://t.
126
126
  | Model and thinking | Switch model or thinking level from Telegram through safe continuation flows. | Mobile control can adjust execution strategy without tearing down the current session. |
127
127
  | Compaction | Confirm `/compact`, show native active status during compaction, and preserve Telegram-owned turn semantics. | Context maintenance is visible and safe from the phone. |
128
128
  | Draft previews | Show Telegram's native `…typing` indicator whenever the connected instance is doing agent work, or enable Rich Draft previews for streamed answer text. | Local prompts, Telegram turns, and autonomous continuations remain visibly active while draft visibility stays independent from final rendering. |
129
- | Activity | Keep the default `verbose` technical surface, show only `thinking`, show only `tools`, or select `quiet` for answer-only delivery. Every instance reloads this shared file-backed choice before a new agent run; thinking uses a headerless expandable quote, while each tool uses one iconless closed root row containing nested evidence details. | Persistent collapsed technical activity minimizes chat height and stays bounded, redacted, target-fenced, free of URL previews, and visually separate from semantic assistant answers. |
129
+ | Activity | Keep the default `verbose` technical surface, show only `thinking`, show only `tools`, or select `quiet` for answer-only delivery. Every instance reloads this shared file-backed choice before a new agent run; thinking accumulates for two seconds and then updates at most every two seconds in a headerless expandable quote, while each tool uses one iconless closed root row containing nested evidence details. | Persistent collapsed technical activity minimizes chat height and stays bounded, redacted, target-fenced, free of URL previews, and visually separate from semantic assistant answers. |
130
130
  | Assistant rendering | Choose Native Rich Markdown or legacy Markdown-to-HTML for final assistant replies. | Renderer compatibility is explicit instead of being conflated with draft previews. |
131
131
  | Bridge UI rendering | Render thinking through headerless expandable HTML with inline emphasis/code, render each tool as an iconless native Rich root details tree with immediately visible arguments and collapsed secondary evidence, and keep menus, queue controls, status, settings, diagnostics, and sections on Telegram HTML/plain UI. | Harness-owned surfaces remain operationally predictable and visually distinct from model-authored answers. |
132
132
  | Inbound files | Download inbound files to the Pi agent temp directory with size limits. | Screenshots, PDFs, datasets, and artifacts enter Pi as inspectable local files. |
@@ -11,7 +11,7 @@ export declare const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3900;
11
11
  export declare const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
12
12
  export declare const TELEGRAM_REASONING_MESSAGE_MAX_FRAMES = 24;
13
13
  export declare const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1200;
14
- export declare const TELEGRAM_REASONING_MIN_INTERVAL_MS = 1200;
14
+ export declare const TELEGRAM_REASONING_MIN_INTERVAL_MS = 2000;
15
15
  export declare const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
16
16
  interface ToolActivity {
17
17
  id: string;
@@ -41,6 +41,8 @@ export declare function createTelegramActivityVerbosityRuntime<TAuthority>(deps:
41
41
  getActivityMode: () => "quiet" | "thinking" | "tools" | "verbose";
42
42
  refreshActivityMode?: () => Promise<void>;
43
43
  getNowMs?: () => number;
44
+ setReasoningTimeout?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
45
+ clearReasoningTimeout?: (timer: ReturnType<typeof setTimeout>) => void;
44
46
  resolveTarget: (event: TelegramActivityEvent) => TelegramTarget | undefined;
45
47
  captureAuthority: () => TAuthority;
46
48
  isAuthorityActive: (authority: TAuthority) => boolean;
@@ -9,7 +9,9 @@ export const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3_900;
9
9
  export const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
10
10
  export const TELEGRAM_REASONING_MESSAGE_MAX_FRAMES = 24;
11
11
  export const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1_200;
12
- export const TELEGRAM_REASONING_MIN_INTERVAL_MS = 1_200;
12
+ // Match native answer drafts: accumulate the opening frame for one full
13
+ // interval, then publish at most one updated frame per interval.
14
+ export const TELEGRAM_REASONING_MIN_INTERVAL_MS = 2_000;
13
15
  export const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
14
16
  function targetEquals(left, right) {
15
17
  return left.chatId === right.chatId && left.threadId === right.threadId;
@@ -234,10 +236,18 @@ export function createTelegramActivityVerbosityRuntime(deps) {
234
236
  let reasoningMessage;
235
237
  let reasoningBlocked = false;
236
238
  let lastReasoningPublishMs = 0;
239
+ let reasoningFlushTimer;
237
240
  let toolMessage;
238
241
  const tools = new Map();
239
242
  const toolOrder = [];
243
+ const clearReasoningFlushTimer = () => {
244
+ if (!reasoningFlushTimer)
245
+ return;
246
+ (deps.clearReasoningTimeout ?? clearTimeout)(reasoningFlushTimer);
247
+ reasoningFlushTimer = undefined;
248
+ };
240
249
  const clearActivity = () => {
250
+ clearReasoningFlushTimer();
241
251
  activityId = undefined;
242
252
  authority = undefined;
243
253
  target = undefined;
@@ -330,6 +340,30 @@ export function createTelegramActivityVerbosityRuntime(deps) {
330
340
  deps.recordFailure?.(canEdit ? "reasoning-edit" : "reasoning-send", event, error);
331
341
  }
332
342
  };
343
+ const scheduleReasoningPublish = (event, acceptedGeneration) => {
344
+ if (reasoningFlushTimer || reasoningBlocked)
345
+ return;
346
+ const elapsed = reasoningMessageFrames === 0
347
+ ? 0
348
+ : getNowMs() - lastReasoningPublishMs;
349
+ const delayMs = Math.max(0, TELEGRAM_REASONING_MIN_INTERVAL_MS - elapsed);
350
+ const schedule = deps.setReasoningTimeout ?? setTimeout;
351
+ reasoningFlushTimer = schedule(() => {
352
+ reasoningFlushTimer = undefined;
353
+ const admittedAuthority = authority;
354
+ const task = async () => {
355
+ if (!isCurrent(acceptedGeneration, admittedAuthority) ||
356
+ reasoningChars <= lastReasoningMessageChars)
357
+ return;
358
+ await publishReasoning(event, acceptedGeneration);
359
+ };
360
+ const enqueue = deps.enqueue ?? ((next) => tail.then(next));
361
+ tail = enqueue(task).catch((error) => {
362
+ deps.recordFailure?.("reasoning-send", event, error);
363
+ });
364
+ }, delayMs);
365
+ reasoningFlushTimer?.unref?.();
366
+ };
333
367
  const publishTool = async (event, tool, acceptedGeneration) => {
334
368
  const admittedAuthority = authority;
335
369
  if (!isCurrent(acceptedGeneration, admittedAuthority) || !target) {
@@ -461,12 +495,8 @@ export function createTelegramActivityVerbosityRuntime(deps) {
461
495
  return;
462
496
  reasoningChars += event.delta.length;
463
497
  reasoningBuffer = `${reasoningBuffer}${event.delta}`.slice(-TELEGRAM_REASONING_BUFFER_MAX_CHARS);
464
- if (reasoningMessageFrames < TELEGRAM_REASONING_MESSAGE_MAX_FRAMES &&
465
- (reasoningMessageFrames === 0 ||
466
- (getNowMs() - lastReasoningPublishMs >=
467
- TELEGRAM_REASONING_MIN_INTERVAL_MS &&
468
- reasoningChars - lastReasoningMessageChars >= 160))) {
469
- await publishReasoning(event, acceptedGeneration);
498
+ if (reasoningMessageFrames < TELEGRAM_REASONING_MESSAGE_MAX_FRAMES) {
499
+ scheduleReasoningPublish(event, acceptedGeneration);
470
500
  }
471
501
  return;
472
502
  }
@@ -477,6 +507,7 @@ export function createTelegramActivityVerbosityRuntime(deps) {
477
507
  reasoningChars = event.text.length;
478
508
  reasoningBuffer = event.text.slice(-TELEGRAM_REASONING_BUFFER_MAX_CHARS);
479
509
  }
510
+ clearReasoningFlushTimer();
480
511
  if (reasoningChars > 0 &&
481
512
  reasoningChars > lastReasoningMessageChars &&
482
513
  !reasoningBlocked) {
@@ -550,8 +581,8 @@ export function createTelegramActivityVerbosityRuntime(deps) {
550
581
  return;
551
582
  }
552
583
  if (event.type === "agent-end" || event.type === "agent-settled") {
553
- if (reasoningMessage &&
554
- reasoningChars > lastReasoningMessageChars &&
584
+ clearReasoningFlushTimer();
585
+ if (reasoningChars > lastReasoningMessageChars &&
555
586
  !reasoningBlocked) {
556
587
  await publishReasoning(event, acceptedGeneration);
557
588
  }
@@ -574,8 +574,9 @@ export declare function createTelegramCommandHandler<TMessage extends TelegramCo
574
574
  export declare function createTelegramCommandOrPromptRuntime<TMessage, TContext>(deps: TelegramCommandOrPromptRuntimeDeps<TMessage, TContext>): {
575
575
  dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
576
576
  };
577
- export declare const TELEGRAM_SESSION_ACTION_COMMAND_NAME = "telegram-session-action";
578
- export declare const TELEGRAM_SESSION_ACTION_COMMAND_DESCRIPTION = "(internal) replace the current Pi session after Telegram settlement";
577
+ export declare const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
578
+ export declare const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "(internal) dispatch one settled Telegram lifecycle action";
579
+ export declare const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram command cannot be run manually.";
579
580
  export declare function delayTelegramSessionAction(delayMs: number): Promise<void>;
580
581
  export interface TelegramSessionActionRuntimeDeps {
581
582
  registerCommand: Pi.ExtensionAPI["registerCommand"];
@@ -1232,8 +1232,9 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
1232
1232
  },
1233
1233
  }, commandArgs);
1234
1234
  }
1235
- export const TELEGRAM_SESSION_ACTION_COMMAND_NAME = "telegram-session-action";
1236
- export const TELEGRAM_SESSION_ACTION_COMMAND_DESCRIPTION = "(internal) replace the current Pi session after Telegram settlement";
1235
+ export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
1236
+ export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "(internal) dispatch one settled Telegram lifecycle action";
1237
+ export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram command cannot be run manually.";
1237
1238
  export function delayTelegramSessionAction(delayMs) {
1238
1239
  return new Promise((resolve) => setTimeout(resolve, delayMs));
1239
1240
  }
@@ -1364,8 +1365,7 @@ export function createTelegramSessionActionAssembly(deps) {
1364
1365
  export function createTelegramSessionActionRuntime(deps) {
1365
1366
  let pendingUpdateId;
1366
1367
  let pendingTarget;
1367
- let commandPending = false;
1368
- let commandUpdateId;
1368
+ let pendingAction;
1369
1369
  let registered = false;
1370
1370
  const reportFailure = (error) => {
1371
1371
  try {
@@ -1380,33 +1380,34 @@ export function createTelegramSessionActionRuntime(deps) {
1380
1380
  if (registered)
1381
1381
  return;
1382
1382
  registered = true;
1383
- deps.registerCommand(TELEGRAM_SESSION_ACTION_COMMAND_NAME, {
1384
- description: TELEGRAM_SESSION_ACTION_COMMAND_DESCRIPTION,
1383
+ deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
1384
+ description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
1385
1385
  handler: async (_args, ctx) => {
1386
- if (!commandPending)
1386
+ const action = pendingAction;
1387
+ if (!action) {
1388
+ ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
1387
1389
  return;
1388
- commandPending = false;
1389
- const target = pendingTarget;
1390
- const updateId = commandUpdateId;
1391
- pendingTarget = undefined;
1392
- commandUpdateId = undefined;
1393
- if (!target || updateId === undefined)
1394
- return;
1395
- try {
1396
- await deps.prepareReplacement?.(ctx, updateId, target);
1397
- const result = await ctx.newSession();
1398
- if (result.cancelled)
1399
- await deps.notifyResult(target, "cancelled");
1400
1390
  }
1401
- catch (error) {
1402
- reportFailure(error);
1403
- await deps.notifyResult(target, "failure");
1391
+ pendingAction = undefined;
1392
+ switch (action.kind) {
1393
+ case "replace-session":
1394
+ try {
1395
+ await deps.prepareReplacement?.(ctx, action.updateId, action.target);
1396
+ const result = await ctx.newSession();
1397
+ if (result.cancelled)
1398
+ await deps.notifyResult(action.target, "cancelled");
1399
+ }
1400
+ catch (error) {
1401
+ reportFailure(error);
1402
+ await deps.notifyResult(action.target, "failure");
1403
+ }
1404
+ return;
1404
1405
  }
1405
1406
  },
1406
1407
  });
1407
1408
  },
1408
1409
  scheduleAfterUpdate(updateId, target) {
1409
- if (pendingUpdateId !== undefined || commandPending)
1410
+ if (pendingUpdateId !== undefined || pendingAction !== undefined)
1410
1411
  return false;
1411
1412
  pendingUpdateId = updateId;
1412
1413
  pendingTarget = { ...target };
@@ -1416,21 +1417,22 @@ export function createTelegramSessionActionRuntime(deps) {
1416
1417
  if (pendingUpdateId !== updateId)
1417
1418
  return;
1418
1419
  pendingUpdateId = undefined;
1419
- commandUpdateId = updateId;
1420
- commandPending = true;
1420
+ const target = pendingTarget;
1421
+ pendingTarget = undefined;
1422
+ if (!target)
1423
+ return;
1424
+ pendingAction = { kind: "replace-session", updateId, target };
1421
1425
  void Promise.resolve()
1422
- .then(() => deps.sendUserMessage(`/${TELEGRAM_SESSION_ACTION_COMMAND_NAME}`, {
1426
+ .then(() => deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME}`, {
1423
1427
  expandPromptTemplates: true,
1424
1428
  }))
1425
1429
  .catch((error) => {
1426
- commandPending = false;
1427
- commandUpdateId = undefined;
1428
- pendingTarget = undefined;
1430
+ pendingAction = undefined;
1429
1431
  reportFailure(error);
1430
1432
  });
1431
1433
  },
1432
1434
  hasPending() {
1433
- return pendingUpdateId !== undefined || commandPending;
1435
+ return pendingUpdateId !== undefined || pendingAction !== undefined;
1434
1436
  },
1435
1437
  };
1436
1438
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -610,7 +610,7 @@ Queue reaction behavior, lane-tail transitions, Keep/Skip independence, multi-re
610
610
 
611
611
  Complete intermediate assistant text blocks from Telegram-originated activity are sent once to the immutable originating target before active-turn final delivery; final and terminal-partial segments stay with settlement so replies are not duplicated. While this instance has exact direct or follower transport authority, completed public blocks from local/autonomous work are always sent once and in source order to the instance's authorized target. Connected companion projection is not configurable; disconnect or authority loss is its boundary. Both paths use the configured Rich or HTML renderer and exclude reasoning, tool traffic, token deltas, local prompt text, unknown sources, and stale generations. Each admitted block remains fenced to its exact target, profile/token stamp, leader epoch or follower registration generation, and session generation; non-idempotent acknowledgement ambiguity never authorizes replay.
612
612
 
613
- `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Bridge-owned assistant output and activity projection share one activity-publication sequence admitted synchronously from the activity bus, so later tool disclosures cannot overtake earlier assistant blocks. Activity targets and transport authority are captured before queued work starts. Active Telegram-turn final replies/artifacts and automatic compaction notices use that same publication owner before the existing direct/follower transport split, without mutually waiting on separate output tails. Final settlement captures the exact active turn and assistant result and reserves its publication position before config loading. Empty outcomes without a publication candidate reserve no position. Replacement, preparation failure, or an unused reservation releases the position without resetting or replying for a replacement turn. Final errors also publish through this owner. Reservations accept one task; cancellation cannot undo a published task, and session reset releases unresolved reservations while fencing old queued tasks. Terminal assistant messages with a publication candidate reserve the final position synchronously; settlement consumes that reservation only for the exact originating turn. Compaction notices enter the same queue immediately, behind the reserved final rather than through a separate notice buffer. Settlement, a new agent run, or session replacement cancels an unconsumed reservation. Notice target and authority are captured when the event is observed. Pi lifecycle completion does not wait for network publication. This order is process-local and does not serialize unrelated instances or independent registered activity handlers. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
613
+ `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Like answer drafts, it accumulates for two seconds before the first frame, publishes at most one update every two seconds, and flushes remaining buffered reasoning on terminal completion. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Bridge-owned assistant output and activity projection share one activity-publication sequence admitted synchronously from the activity bus, so later tool disclosures cannot overtake earlier assistant blocks. Activity targets and transport authority are captured before queued work starts. Active Telegram-turn final replies/artifacts and automatic compaction notices use that same publication owner before the existing direct/follower transport split, without mutually waiting on separate output tails. Final settlement captures the exact active turn and assistant result and reserves its publication position before config loading. Empty outcomes without a publication candidate reserve no position. Replacement, preparation failure, or an unused reservation releases the position without resetting or replying for a replacement turn. Final errors also publish through this owner. Reservations accept one task; cancellation cannot undo a published task, and session reset releases unresolved reservations while fencing old queued tasks. Terminal assistant messages with a publication candidate reserve the final position synchronously; settlement consumes that reservation only for the exact originating turn. Compaction notices enter the same queue immediately, behind the reserved final rather than through a separate notice buffer. Settlement, a new agent run, or session replacement cancels an unconsumed reservation. Notice target and authority are captured when the event is observed. Pi lifecycle completion does not wait for network publication. This order is process-local and does not serialize unrelated instances or independent registered activity handlers. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
614
614
 
615
615
  Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension contribute `telegram-bridge`, optional `generated-control-surface`, and `generative-apps` Skills through Pi resource discovery. Generated Control Surface treats `interface = f(state, capabilities, intent)` as a renderer-neutral primitive, compiling transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. It composes an ordered ragged sequence of independently sized semantic rows rather than filling a rectangular grid: compact rows contain genuine peers, singleton rows isolate structurally independent actions, and rectangular layouts remain reserved for genuinely spatial state. Text-bearing controls use at most two columns and flow into additional rows, while denser rows are reserved for short position-bearing glyphs or codes and never exceed the eight-column phone-width UX maximum. Vertical extent is independent: a true spatial surface may retain substantially more rows, while non-spatial button walls route to grouping, disclosure, or pagination. Symmetry is treated as an evidence claim about equal relationships or real spatial topology; an abstract layout catalog supplies adaptable singleton, peer, staged, navigational, repeated-pair, and rectangular shapes without forcing tasks into preset grids. Repeated controls carry the smallest sufficient action delta when visible conversation is unambiguous; larger or error-prone state moves to a deterministic task-owned Markdown artifact, correctness-sensitive transitions move to a small domain-owned transition implementation, and repeated clicks are adjudicated against current state rather than stale button appearance. Its filesystem adapter reserves the first full-width row for parent traversal outside root, places available Previous/Next controls together in one compact row immediately afterward, orders visible directories, hidden directories, visible files, and hidden files alphabetically within each category before fixed ten-entry pagination, renders path/range metadata as stacked status-style key-value rows instead of middle-dot prose, emits the complete Telegram control set through one JSON-matrix action, suppresses duplicate plain/monospaced listings and default Refresh unless user preference overrides presentation, and retains an ordinary numbered fallback when buttons are unavailable. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Repeated stable interactions may graduate from that model-mediated surface into a reviewed Generative App whose deterministic bound methods bypass Pi queue admission; the `generative-apps` Skill owns this compilation and operating workflow while the underlying capability retains domain authority. Telegram-originated turns route to the stable Skill contracts and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skills and public documentation own syntax, target routing, Threaded Mode behavior, Generative App operation, and diagnostics.
616
616
 
@@ -51,7 +51,7 @@ Stable commands inside Pi:
51
51
  Stable commands inside the paired Telegram DM:
52
52
 
53
53
  - `/start` — pair when needed and open the main application menu.
54
- - `/new` — after idle and empty-queue checks, request a new Pi session in the current classic chat or Thread. The bridge acknowledges the callback, deletes its confirmation, completes and removes the exact durable update, then dispatches an internal Pi command through `pi.sendUserMessage(..., { expandPromptTemplates: true })`. Its real `ExtensionCommandContext` calls `ctx.newSession()`; one discriminated durable intent preserves either exact classic Profile/CWD/session/chat continuity or the Thread binding with slot/name re-key. The successor CAS-claims that intent before sending one terminal result. Busy, identity-mismatch, and unavailable-host paths fail closed.
54
+ - `/new` — after idle and empty-queue checks, request a new Pi session in the current classic chat or Thread. The bridge acknowledges the callback, deletes its confirmation, completes and removes the exact durable update, then dispatches one runtime-armed typed action through the internal `/telegram-internal` Pi gateway using `pi.sendUserMessage(..., { expandPromptTemplates: true })`. Manual invocation reports that the gateway cannot be run manually; the armed handler receives a real `ExtensionCommandContext` and calls `ctx.newSession()`; one discriminated durable intent preserves either exact classic Profile/CWD/session/chat continuity or the Thread binding with slot/name re-key. The successor CAS-claims that intent before sending one terminal result. Busy, identity-mismatch, and unavailable-host paths fail closed.
55
55
  - `/compact` — open confirmation and compact when idle.
56
56
  - `/next` — abort active work first when needed, let the interrupted prompt receive its abort notice, then reply `Dispatching next queued turn.` to the exact queued prompt selected for the next model turn. The command itself is never the lifecycle-notice reply target, and aborted pending assistant text is suppressed.
57
57
  - `/continue` — enqueue a priority `continue` prompt.
@@ -24,7 +24,9 @@ export const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3_900;
24
24
  export const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
25
25
  export const TELEGRAM_REASONING_MESSAGE_MAX_FRAMES = 24;
26
26
  export const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1_200;
27
- export const TELEGRAM_REASONING_MIN_INTERVAL_MS = 1_200;
27
+ // Match native answer drafts: accumulate the opening frame for one full
28
+ // interval, then publish at most one updated frame per interval.
29
+ export const TELEGRAM_REASONING_MIN_INTERVAL_MS = 2_000;
28
30
  export const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
29
31
 
30
32
  interface ToolActivity {
@@ -325,6 +327,11 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
325
327
  getActivityMode: () => "quiet" | "thinking" | "tools" | "verbose";
326
328
  refreshActivityMode?: () => Promise<void>;
327
329
  getNowMs?: () => number;
330
+ setReasoningTimeout?: (
331
+ callback: () => void,
332
+ delayMs: number,
333
+ ) => ReturnType<typeof setTimeout>;
334
+ clearReasoningTimeout?: (timer: ReturnType<typeof setTimeout>) => void;
328
335
  resolveTarget: (event: TelegramActivityEvent) => TelegramTarget | undefined;
329
336
  captureAuthority: () => TAuthority;
330
337
  isAuthorityActive: (authority: TAuthority) => boolean;
@@ -360,11 +367,18 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
360
367
  let reasoningMessage: ReasoningMessage | undefined;
361
368
  let reasoningBlocked = false;
362
369
  let lastReasoningPublishMs = 0;
370
+ let reasoningFlushTimer: ReturnType<typeof setTimeout> | undefined;
363
371
  let toolMessage: ToolMessage | undefined;
364
372
  const tools = new Map<string, ToolActivity>();
365
373
  const toolOrder: string[] = [];
366
374
 
375
+ const clearReasoningFlushTimer = () => {
376
+ if (!reasoningFlushTimer) return;
377
+ (deps.clearReasoningTimeout ?? clearTimeout)(reasoningFlushTimer);
378
+ reasoningFlushTimer = undefined;
379
+ };
367
380
  const clearActivity = () => {
381
+ clearReasoningFlushTimer();
368
382
  activityId = undefined;
369
383
  authority = undefined;
370
384
  target = undefined;
@@ -466,6 +480,33 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
466
480
  );
467
481
  }
468
482
  };
483
+ const scheduleReasoningPublish = (
484
+ event: TelegramActivityEvent,
485
+ acceptedGeneration: number,
486
+ ) => {
487
+ if (reasoningFlushTimer || reasoningBlocked) return;
488
+ const elapsed = reasoningMessageFrames === 0
489
+ ? 0
490
+ : getNowMs() - lastReasoningPublishMs;
491
+ const delayMs = Math.max(0, TELEGRAM_REASONING_MIN_INTERVAL_MS - elapsed);
492
+ const schedule = deps.setReasoningTimeout ?? setTimeout;
493
+ reasoningFlushTimer = schedule(() => {
494
+ reasoningFlushTimer = undefined;
495
+ const admittedAuthority = authority;
496
+ const task = async () => {
497
+ if (
498
+ !isCurrent(acceptedGeneration, admittedAuthority) ||
499
+ reasoningChars <= lastReasoningMessageChars
500
+ ) return;
501
+ await publishReasoning(event, acceptedGeneration);
502
+ };
503
+ const enqueue = deps.enqueue ?? ((next: () => Promise<void>) => tail.then(next));
504
+ tail = enqueue(task).catch((error) => {
505
+ deps.recordFailure?.("reasoning-send", event, error);
506
+ });
507
+ }, delayMs) as ReturnType<typeof setTimeout>;
508
+ reasoningFlushTimer?.unref?.();
509
+ };
469
510
  const publishTool = async (
470
511
  event: TelegramActivityEvent,
471
512
  tool: ToolActivity,
@@ -602,13 +643,8 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
602
643
  reasoningBuffer = `${reasoningBuffer}${event.delta}`.slice(
603
644
  -TELEGRAM_REASONING_BUFFER_MAX_CHARS,
604
645
  );
605
- if (reasoningMessageFrames < TELEGRAM_REASONING_MESSAGE_MAX_FRAMES &&
606
- (reasoningMessageFrames === 0 ||
607
- (getNowMs() - lastReasoningPublishMs >=
608
- TELEGRAM_REASONING_MIN_INTERVAL_MS &&
609
- reasoningChars - lastReasoningMessageChars >= 160))
610
- ) {
611
- await publishReasoning(event, acceptedGeneration);
646
+ if (reasoningMessageFrames < TELEGRAM_REASONING_MESSAGE_MAX_FRAMES) {
647
+ scheduleReasoningPublish(event, acceptedGeneration);
612
648
  }
613
649
  return;
614
650
  }
@@ -620,6 +656,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
620
656
  -TELEGRAM_REASONING_BUFFER_MAX_CHARS,
621
657
  );
622
658
  }
659
+ clearReasoningFlushTimer();
623
660
  if (
624
661
  reasoningChars > 0 &&
625
662
  reasoningChars > lastReasoningMessageChars &&
@@ -687,8 +724,8 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
687
724
  return;
688
725
  }
689
726
  if (event.type === "agent-end" || event.type === "agent-settled") {
727
+ clearReasoningFlushTimer();
690
728
  if (
691
- reasoningMessage &&
692
729
  reasoningChars > lastReasoningMessageChars &&
693
730
  !reasoningBlocked
694
731
  ) {
@@ -2335,9 +2335,11 @@ async function handleTelegramCommandRuntime<
2335
2335
  );
2336
2336
  }
2337
2337
 
2338
- export const TELEGRAM_SESSION_ACTION_COMMAND_NAME = "telegram-session-action";
2339
- export const TELEGRAM_SESSION_ACTION_COMMAND_DESCRIPTION =
2340
- "(internal) replace the current Pi session after Telegram settlement";
2338
+ export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
2339
+ export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION =
2340
+ "(internal) dispatch one settled Telegram lifecycle action";
2341
+ export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE =
2342
+ "This internal Telegram command cannot be run manually.";
2341
2343
 
2342
2344
  export function delayTelegramSessionAction(delayMs: number): Promise<void> {
2343
2345
  return new Promise<void>((resolve) => setTimeout(resolve, delayMs));
@@ -2538,6 +2540,12 @@ export function createTelegramSessionActionAssembly(
2538
2540
  return { action, settlement };
2539
2541
  }
2540
2542
 
2543
+ type TelegramPendingInternalAction = {
2544
+ kind: "replace-session";
2545
+ updateId: number;
2546
+ target: { chatId: number; threadId?: number; messageId: number };
2547
+ };
2548
+
2541
2549
  export interface TelegramSessionActionRuntime {
2542
2550
  register: () => void;
2543
2551
  scheduleAfterUpdate: (
@@ -2557,8 +2565,7 @@ export function createTelegramSessionActionRuntime(
2557
2565
  threadId?: number;
2558
2566
  messageId: number;
2559
2567
  } | undefined;
2560
- let commandPending = false;
2561
- let commandUpdateId: number | undefined;
2568
+ let pendingAction: TelegramPendingInternalAction | undefined;
2562
2569
  let registered = false;
2563
2570
 
2564
2571
  const reportFailure = (error: unknown): void => {
@@ -2573,29 +2580,32 @@ export function createTelegramSessionActionRuntime(
2573
2580
  register() {
2574
2581
  if (registered) return;
2575
2582
  registered = true;
2576
- deps.registerCommand(TELEGRAM_SESSION_ACTION_COMMAND_NAME, {
2577
- description: TELEGRAM_SESSION_ACTION_COMMAND_DESCRIPTION,
2583
+ deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
2584
+ description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
2578
2585
  handler: async (_args, ctx) => {
2579
- if (!commandPending) return;
2580
- commandPending = false;
2581
- const target = pendingTarget;
2582
- const updateId = commandUpdateId;
2583
- pendingTarget = undefined;
2584
- commandUpdateId = undefined;
2585
- if (!target || updateId === undefined) return;
2586
- try {
2587
- await deps.prepareReplacement?.(ctx, updateId, target);
2588
- const result = await ctx.newSession();
2589
- if (result.cancelled) await deps.notifyResult(target, "cancelled");
2590
- } catch (error) {
2591
- reportFailure(error);
2592
- await deps.notifyResult(target, "failure");
2586
+ const action = pendingAction;
2587
+ if (!action) {
2588
+ ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
2589
+ return;
2590
+ }
2591
+ pendingAction = undefined;
2592
+ switch (action.kind) {
2593
+ case "replace-session":
2594
+ try {
2595
+ await deps.prepareReplacement?.(ctx, action.updateId, action.target);
2596
+ const result = await ctx.newSession();
2597
+ if (result.cancelled) await deps.notifyResult(action.target, "cancelled");
2598
+ } catch (error) {
2599
+ reportFailure(error);
2600
+ await deps.notifyResult(action.target, "failure");
2601
+ }
2602
+ return;
2593
2603
  }
2594
2604
  },
2595
2605
  });
2596
2606
  },
2597
2607
  scheduleAfterUpdate(updateId, target) {
2598
- if (pendingUpdateId !== undefined || commandPending) return false;
2608
+ if (pendingUpdateId !== undefined || pendingAction !== undefined) return false;
2599
2609
  pendingUpdateId = updateId;
2600
2610
  pendingTarget = { ...target };
2601
2611
  return true;
@@ -2603,23 +2613,23 @@ export function createTelegramSessionActionRuntime(
2603
2613
  onUpdateCompleted(updateId) {
2604
2614
  if (pendingUpdateId !== updateId) return;
2605
2615
  pendingUpdateId = undefined;
2606
- commandUpdateId = updateId;
2607
- commandPending = true;
2616
+ const target = pendingTarget;
2617
+ pendingTarget = undefined;
2618
+ if (!target) return;
2619
+ pendingAction = { kind: "replace-session", updateId, target };
2608
2620
  void Promise.resolve()
2609
2621
  .then(() =>
2610
- deps.sendUserMessage(`/${TELEGRAM_SESSION_ACTION_COMMAND_NAME}`, {
2622
+ deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME}`, {
2611
2623
  expandPromptTemplates: true,
2612
2624
  }),
2613
2625
  )
2614
2626
  .catch((error) => {
2615
- commandPending = false;
2616
- commandUpdateId = undefined;
2617
- pendingTarget = undefined;
2627
+ pendingAction = undefined;
2618
2628
  reportFailure(error);
2619
2629
  });
2620
2630
  },
2621
2631
  hasPending() {
2622
- return pendingUpdateId !== undefined || commandPending;
2632
+ return pendingUpdateId !== undefined || pendingAction !== undefined;
2623
2633
  },
2624
2634
  };
2625
2635
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
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.17.1",
3
+ "version": "0.18.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -45,7 +45,7 @@
45
45
  "@llblab/pi-codex-usage": "0.10.0",
46
46
  "@llblab/pi-grow-loop": "0.8.1",
47
47
  "@llblab/pi-state-flow": "0.16.2",
48
- "@llblab/pi-telegram": "0.49.0",
48
+ "@llblab/pi-telegram": "0.50.0",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },
51
51
  "bundledDependencies": [