@llblab/pi-telegram 0.36.8 → 0.36.10
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 +9 -0
- package/index.ts +9 -0
- package/lib/runtime.ts +52 -2
- package/lib/status.ts +3 -3
- package/package.json +1 -1
- package/skills/generated-control-surface/SKILL.md +14 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.36.10: Transport-Fenced Typing Hotfix
|
|
6
|
+
|
|
7
|
+
- `Typing Authority Fence`: Starts and continues Telegram typing activity only while the Pi instance has direct ownership or a live follower registration, stopping quietly when authority disappears so classic takeover cannot re-arm a stale loop or flood diagnostics with expected follower-registration errors.
|
|
8
|
+
|
|
9
|
+
## 0.36.9: Telegram Status And Generated Controls Hotfix
|
|
10
|
+
|
|
11
|
+
- `Classic Takeover Status`: Keeps a classic-mode client visibly `disconnected` after another Pi instance takes Telegram ownership, preventing stale transport-side activity errors from overriding the authoritative connection state.
|
|
12
|
+
- `Generated Button Surfaces`: Makes vertical full-width controls the phone-readable default, earns multi-column rows only for compact labels or emoji-only spatial controls, encourages concise semantic labels, and requires safe 2–6 button controls whenever a Telegram reply asks a bounded blocking confirmation or choice instead of leaving an avoidable prose-only feedback step.
|
|
13
|
+
|
|
5
14
|
## 0.36.8: Durable Journal Recovery
|
|
6
15
|
|
|
7
16
|
- `Durable Journal Recovery`: Restricts age cleanup to UUID-prefixed downloads, repairs missing or revisionless snapshots from validated segment evidence, and otherwise quarantines the snapshot plus segments before publishing a fresh journal, keeping `/telegram-connect` operational with informational recovery evidence instead of manual JSON repair.
|
package/index.ts
CHANGED
|
@@ -465,6 +465,15 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
465
465
|
BusApi.createTelegramAggregateTypingActionSender(telegramApiRuntime),
|
|
466
466
|
updateStatus,
|
|
467
467
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
468
|
+
getTransportAuthority() {
|
|
469
|
+
if (ownsTelegramDirectDelivery()) {
|
|
470
|
+
const epoch = getCurrentLeaderEpoch();
|
|
471
|
+
return epoch === undefined ? undefined : `direct:${epoch}`;
|
|
472
|
+
}
|
|
473
|
+
if (!telegramBusFollowerRegistrationState.isRegistered()) return undefined;
|
|
474
|
+
const generation = telegramBusFollowerRegistrationState.getGeneration();
|
|
475
|
+
return generation ? `follower:${generation}` : undefined;
|
|
476
|
+
},
|
|
468
477
|
recordRuntimeEvent,
|
|
469
478
|
});
|
|
470
479
|
const currentModelRuntime = Model.createCurrentModelRuntime({
|
package/lib/runtime.ts
CHANGED
|
@@ -320,6 +320,8 @@ export interface TelegramTypingLoopDeps {
|
|
|
320
320
|
options?: { message_thread_id?: number },
|
|
321
321
|
) => Promise<unknown>;
|
|
322
322
|
sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
|
|
323
|
+
shouldContinue?: () => boolean;
|
|
324
|
+
onStopped?: () => void;
|
|
323
325
|
}
|
|
324
326
|
|
|
325
327
|
export interface TelegramRuntimeEventRecorderPort {
|
|
@@ -365,6 +367,8 @@ export interface TelegramTypingLoopStarterDeps<
|
|
|
365
367
|
sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
|
|
366
368
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
367
369
|
isContextActive?: (ctx: TContext) => boolean;
|
|
370
|
+
isTransportAvailable?: () => boolean;
|
|
371
|
+
getTransportAuthority?: () => string | number | undefined;
|
|
368
372
|
intervalMs?: number;
|
|
369
373
|
}
|
|
370
374
|
|
|
@@ -376,15 +380,33 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
376
380
|
options?: { target?: TelegramTypingLoopTarget },
|
|
377
381
|
) => void {
|
|
378
382
|
return (ctx, chatId, options) => {
|
|
383
|
+
const transportAuthority = deps.getTransportAuthority?.();
|
|
384
|
+
const hasTransport = (): boolean =>
|
|
385
|
+
deps.getTransportAuthority
|
|
386
|
+
? transportAuthority !== undefined &&
|
|
387
|
+
Object.is(deps.getTransportAuthority(), transportAuthority)
|
|
388
|
+
: deps.isTransportAvailable?.() !== false;
|
|
389
|
+
if (!hasTransport()) return;
|
|
390
|
+
let active = true;
|
|
379
391
|
deps.typing.start({
|
|
380
392
|
chatId: chatId ?? deps.getDefaultChatId(),
|
|
381
393
|
target: options?.target,
|
|
382
394
|
intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
|
|
383
395
|
sendTypingAction: async (targetChatId, actionOptions) => {
|
|
396
|
+
if (!active) return;
|
|
397
|
+
if (!hasTransport()) {
|
|
398
|
+
deps.typing.stop();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
384
401
|
try {
|
|
385
402
|
await deps.sendTypingAction(targetChatId, actionOptions);
|
|
386
403
|
} catch (error) {
|
|
387
404
|
if (deps.isContextActive?.(ctx) === false) return;
|
|
405
|
+
if (!active) return;
|
|
406
|
+
if (!hasTransport()) {
|
|
407
|
+
deps.typing.stop();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
388
410
|
const message =
|
|
389
411
|
error instanceof Error ? error.message : String(error);
|
|
390
412
|
updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
|
|
@@ -404,10 +426,20 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
404
426
|
},
|
|
405
427
|
sendAggregateTypingAction: deps.sendAggregateTypingAction
|
|
406
428
|
? async (targetChatId) => {
|
|
429
|
+
if (!active) return;
|
|
430
|
+
if (!hasTransport()) {
|
|
431
|
+
deps.typing.stop();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
407
434
|
try {
|
|
408
435
|
await deps.sendAggregateTypingAction?.(targetChatId);
|
|
409
436
|
} catch (error) {
|
|
410
437
|
if (deps.isContextActive?.(ctx) === false) return;
|
|
438
|
+
if (!active) return;
|
|
439
|
+
if (!hasTransport()) {
|
|
440
|
+
deps.typing.stop();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
411
443
|
const message =
|
|
412
444
|
error instanceof Error ? error.message : String(error);
|
|
413
445
|
updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
|
|
@@ -427,6 +459,10 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
427
459
|
}
|
|
428
460
|
}
|
|
429
461
|
: undefined,
|
|
462
|
+
shouldContinue: hasTransport,
|
|
463
|
+
onStopped: () => {
|
|
464
|
+
active = false;
|
|
465
|
+
},
|
|
430
466
|
});
|
|
431
467
|
};
|
|
432
468
|
}
|
|
@@ -442,7 +478,12 @@ export function startTelegramTypingLoop(
|
|
|
442
478
|
): boolean {
|
|
443
479
|
if (deps.chatId === undefined || deps.chatId === 0) return false;
|
|
444
480
|
const previousKey = state.typingLoopKey;
|
|
481
|
+
const previousDeps = state.typingLoopDeps;
|
|
445
482
|
const nextKey = getTelegramTypingLoopKey(deps);
|
|
483
|
+
if (previousDeps && previousDeps !== deps) {
|
|
484
|
+
previousDeps.onStopped?.();
|
|
485
|
+
state.typingInFlight = undefined;
|
|
486
|
+
}
|
|
446
487
|
state.typingLoopDeps = deps;
|
|
447
488
|
state.typingLoopKey = nextKey;
|
|
448
489
|
const sendTyping = (): void => {
|
|
@@ -450,10 +491,14 @@ export function startTelegramTypingLoop(
|
|
|
450
491
|
if (
|
|
451
492
|
!activeDeps ||
|
|
452
493
|
activeDeps.chatId === undefined ||
|
|
453
|
-
activeDeps.chatId === 0
|
|
454
|
-
state.typingInFlight
|
|
494
|
+
activeDeps.chatId === 0
|
|
455
495
|
)
|
|
456
496
|
return;
|
|
497
|
+
if (activeDeps.shouldContinue?.() === false) {
|
|
498
|
+
stopTelegramTypingLoop(state);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (state.typingInFlight) return;
|
|
457
502
|
const targetChatId = activeDeps.chatId;
|
|
458
503
|
const threadParams = getTelegramTypingLoopThreadParams(activeDeps.target);
|
|
459
504
|
const typing = Promise.resolve()
|
|
@@ -486,9 +531,12 @@ export function stopTelegramTypingLoop(
|
|
|
486
531
|
): boolean {
|
|
487
532
|
if (!state.typingInterval) return false;
|
|
488
533
|
clearInterval(state.typingInterval);
|
|
534
|
+
const activeDeps = state.typingLoopDeps;
|
|
489
535
|
state.typingInterval = undefined;
|
|
490
536
|
state.typingLoopDeps = undefined;
|
|
491
537
|
state.typingLoopKey = undefined;
|
|
538
|
+
state.typingInFlight = undefined;
|
|
539
|
+
activeDeps?.onStopped?.();
|
|
492
540
|
return true;
|
|
493
541
|
}
|
|
494
542
|
|
|
@@ -571,6 +619,8 @@ export interface TelegramPromptDispatchRuntimeDeps<
|
|
|
571
619
|
sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
|
|
572
620
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
573
621
|
isContextActive?: (ctx: TContext) => boolean;
|
|
622
|
+
isTransportAvailable?: () => boolean;
|
|
623
|
+
getTransportAuthority?: () => string | number | undefined;
|
|
574
624
|
intervalMs?: number;
|
|
575
625
|
}
|
|
576
626
|
|
package/lib/status.ts
CHANGED
|
@@ -871,9 +871,6 @@ export function buildTelegramStatusBarText(
|
|
|
871
871
|
state: TelegramStatusBarState,
|
|
872
872
|
): string {
|
|
873
873
|
const label = theme.fg("accent", getTelegramStatusBarLabel(state));
|
|
874
|
-
if (state.error) {
|
|
875
|
-
return `${label} ${theme.fg("error", "error")}`;
|
|
876
|
-
}
|
|
877
874
|
const queued = state.queuedStatus
|
|
878
875
|
? theme.fg("success", state.queuedStatus)
|
|
879
876
|
: "";
|
|
@@ -885,6 +882,9 @@ export function buildTelegramStatusBarText(
|
|
|
885
882
|
return `${label} ${theme.fg("warning", "electing")}${queued}`;
|
|
886
883
|
if (!state.pollingActive && state.busRole !== "follower")
|
|
887
884
|
return `${theme.fg("accent", "telegram")} ${theme.fg("dim", "disconnected")}${queued}`;
|
|
885
|
+
if (state.error) {
|
|
886
|
+
return `${label} ${theme.fg("error", "error")}`;
|
|
887
|
+
}
|
|
888
888
|
if (state.processing) {
|
|
889
889
|
const processingStatus = state.queuedStatus
|
|
890
890
|
? "active"
|
package/package.json
CHANGED
|
@@ -54,9 +54,15 @@ Console programs are one capability source, not the defining boundary. Use the r
|
|
|
54
54
|
|
|
55
55
|
## Control Admission
|
|
56
56
|
|
|
57
|
-
Buttons are optional
|
|
57
|
+
Buttons are optional only when no candidate passes the admission test. Bias toward offering them whenever they materially shorten a likely feedback loop. If the user can approve, reject, refine, prioritize, redirect, inspect, or choose a concrete next step faster by tapping than by composing a reply, proactively expose the smallest useful control set without waiting to be asked for buttons.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
On a Telegram-originated turn, emitting controls is required when the response asks the user for one or more bounded confirmations or choices, the likely answers can be represented truthfully in 2–6 safe controls, and no secret-bearing or high-impact ambiguity blocks their formulation. This includes blocking questions such as confirming scope, version, workflow classification, approval, or the next release step. Ask in prose when explanation is necessary, but attach the controls in the same reply; the availability of free-form typing is not a reason to omit them. A high-impact operation still uses the required confirmation flow rather than a one-tap execution shortcut.
|
|
60
|
+
|
|
61
|
+
Apply the same requirement at a workflow handoff even when the current user message itself was a complete correction or implementation command. Before concluding a Telegram reply, project the user's likely next intent from the active goal, recent trajectory, newly completed act, and available capabilities—not only from an explicitly pending question. Typical phase transitions include “prepare → release”, “inspect → approve”, “fix → rerun”, and “draft → send”. If one next action or a small alternative set is high-confidence, newly unblocked, truthfully expressible, and safe to request, emit 2–6 controls now instead of waiting for the user to restate the obvious next step. A narrowly completed subtask does not erase the parent intent or make the predictive handoff decorative.
|
|
62
|
+
|
|
63
|
+
This is anticipatory interaction, not generic suggestion generation. Prefer controls that advance the user's demonstrated workflow over broad capability menus, speculative side quests, or “What next?” buttons. A predicted high-impact action is offered as an explicit self-contained request or confirmation, never silently executed. When confidence is low or materially different next intents compete, omit controls or expose only the smallest clarifying choice.
|
|
64
|
+
|
|
65
|
+
Zero buttons remains preferable when controls would only decorate the answer, restate visible prose, solicit generic “What next?” input, expose an unclear consequence, save negligible effort, or when the user already issued a complete command and neither that command nor the inferred active workflow leaves a high-confidence immediate decision. A button earns its place by reducing response effort, ambiguity, turnaround time, or supervision cost while preserving an ordinary typed reply as a first-class option.
|
|
60
66
|
|
|
61
67
|
For status requests, show a compact `Refresh` control and bounded inspect/drill-down controls only when work is active, blocked, stale-sensitive, or otherwise actionable. A completed static status needs no buttons. Do not add destructive shortcuts or actions whose target and consequence are not yet clear.
|
|
62
68
|
|
|
@@ -130,11 +136,11 @@ Use the transport's canonical prompt-button syntax. For pi-telegram, one top-lev
|
|
|
130
136
|
|
|
131
137
|
Model the control surface as an ordered ragged sequence of independently sized rows, not as a rectangular matrix to fill. Rectangular grids are one specialization for genuinely spatial or coordinate-bearing state; most interfaces should vary row width according to hierarchy, grouping, label pressure, and action priority.
|
|
132
138
|
|
|
133
|
-
- Put controls in one compact row only when they are genuine peers that answer the same local question or form one coherent toolbar/navigation group.
|
|
134
|
-
- Use a singleton full-width row for a structurally independent, pinned, primary, summary, or high-consequence action
|
|
139
|
+
- Default to one full-width button per row for non-spatial controls. Put controls in one compact row only when they are genuine peers that answer the same local question or form one coherent toolbar/navigation group **and** their rendered labels comfortably fit a narrow phone-width chat.
|
|
140
|
+
- Use a singleton full-width row for a structurally independent, pinned, primary, summary, or high-consequence action, and whenever label length makes horizontal grouping cramped or ambiguous.
|
|
135
141
|
- Vary row widths intentionally—for example `1 → 2 → 4 → 1 → 2`—and never pad a row with empty, duplicate, or no-op controls merely to produce uniform dimensions.
|
|
136
142
|
- Preserve reading order across rows: orientation and structural navigation first, primary content or choices next, secondary controls afterward, and destructive actions visibly separated when present.
|
|
137
|
-
-
|
|
143
|
+
- Treat two columns as an earned compact mode, not the default: a pair normally fits when each label is no more than one emoji plus roughly two average-length words. If either label has more words, unusually long words, qualifiers, or likely wrapping, place each button on its own row. Use at most two columns for readable text labels; move additional peer choices into more semantic rows rather than compressing textual buttons across a phone-width line. Three through five columns are for short symbols, glyphs, coordinates, or compact codes whose position carries meaning. Six through eight may be used only for single-glyph or similarly minimal position-bearing labels whose grouping materially improves the interaction; a row of emoji-only controls may therefore legitimately use up to eight columns. Eight is the phone-width UX maximum: never generate a row of nine or more controls even though the parser has no artificial width cap. Never shorten necessary wording merely to increase row density; regroup or use full-width rows when labels need explanation, wrap ambiguously, or lose meaning without prose.
|
|
138
144
|
|
|
139
145
|
Treat vertical extent independently from horizontal density. A genuinely spatial surface may retain many rows—such as an `8×16` field—when vertical continuity, coordinates, and one-glance topology matter; do not paginate merely to make its height match its width. For non-spatial collections, however, a tall button wall should yield to semantic grouping, progressive disclosure, or pagination. Keep compact state and instructions above a tall surface, preserve stable coordinates across regeneration, and avoid repeating prose between rows.
|
|
140
146
|
|
|
@@ -181,7 +187,7 @@ Button prompts must:
|
|
|
181
187
|
- Request fresh inspection when state may have changed.
|
|
182
188
|
- Avoid embedding volatile output that should be rediscovered.
|
|
183
189
|
|
|
184
|
-
Labels stay short, distinct, and scannable. Emoji are
|
|
190
|
+
Labels stay short, distinct, and scannable. Prefer an explicit `label` over exposing a long prompt as button text. Emoji are explicitly allowed and encouraged when one consistent semantic marker improves scanning or expressiveness; keep their meaning consistent across sibling controls, avoid decorative noise, and do not rely on emoji or color alone. If buttons are unavailable, render the same control surface as a numbered choice list.
|
|
185
191
|
|
|
186
192
|
## Capability Adapters
|
|
187
193
|
|
|
@@ -230,6 +236,8 @@ Buttons may represent explicit alternatives without live system inspection. Stat
|
|
|
230
236
|
|
|
231
237
|
Before sending a surface, verify:
|
|
232
238
|
|
|
239
|
+
- If the reply asks a Telegram user for bounded confirmation or selection, qualifying controls are present; do not ship a prose-only blocking question merely because the answer is short.
|
|
240
|
+
- If the completed act unblocks a high-confidence next intent inferred from the parent goal and workflow trajectory, qualifying handoff controls are present even when no explicit pending question exists and the latest user message was itself a complete command.
|
|
233
241
|
- State and controls share one clear owner and target.
|
|
234
242
|
- Live claims come from current evidence.
|
|
235
243
|
- Complete versus filtered or adapted output is labeled honestly.
|