@llblab/pi-telegram 0.35.1 → 0.36.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/lib/queue.ts CHANGED
@@ -34,7 +34,17 @@ export type TelegramQueueLane = "control" | "priority" | "default";
34
34
  export type TelegramQueueReactionDisposition =
35
35
  | { kind: "default" }
36
36
  | { kind: "priority"; emoji: string }
37
- | { kind: "suppressed"; emoji: string };
37
+ | { kind: "suppressed"; emoji: string }
38
+ | {
39
+ kind: "priority-suppressed";
40
+ priorityEmoji: string;
41
+ suppressionEmoji: string;
42
+ }
43
+ | {
44
+ kind: "reaction-transition";
45
+ priorityEmoji?: string | null;
46
+ suppressionEmoji?: string | null;
47
+ };
38
48
 
39
49
  export interface TelegramQueueAdmissionReceipt {
40
50
  queueKind: TelegramQueueItemKind;
@@ -899,7 +909,7 @@ export function applyTelegramQueuePromptReactionDisposition<
899
909
  items: TelegramQueueItem<TContext>[],
900
910
  messageId: number,
901
911
  disposition: TelegramQueueReactionDisposition,
902
- priorityLaneOrder?: number,
912
+ destinationLaneOrder?: number,
903
913
  scope?: TelegramQueueMessageScope,
904
914
  ): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
905
915
  let nextItems = items;
@@ -911,17 +921,38 @@ export function applyTelegramQueuePromptReactionDisposition<
911
921
  ) {
912
922
  continue;
913
923
  }
914
- const queueLane: TelegramQueueLane =
915
- disposition.kind === "priority" ? "priority" : "default";
916
- const laneOrder =
917
- disposition.kind === "priority" ? priorityLaneOrder : item.queueOrder;
924
+ const isPriority = disposition.kind === "reaction-transition"
925
+ ? disposition.priorityEmoji === undefined
926
+ ? item.queueLane === "priority"
927
+ : disposition.priorityEmoji !== null
928
+ : disposition.kind === "priority" ||
929
+ disposition.kind === "priority-suppressed";
930
+ const queueLane: TelegramQueueLane = isPriority ? "priority" : "default";
931
+ const laneOrder = item.queueLane === queueLane
932
+ ? item.laneOrder
933
+ : destinationLaneOrder;
918
934
  if (laneOrder === undefined) {
919
- throw new Error("Telegram priority reaction order is unavailable.");
935
+ throw new Error("Telegram destination lane order is unavailable.");
920
936
  }
921
- const priorityEmoji =
922
- disposition.kind === "priority" ? disposition.emoji : undefined;
937
+ const priorityEmoji = disposition.kind === "reaction-transition"
938
+ ? disposition.priorityEmoji === undefined
939
+ ? item.priorityEmoji
940
+ : disposition.priorityEmoji ?? undefined
941
+ : disposition.kind === "priority"
942
+ ? disposition.emoji
943
+ : disposition.kind === "priority-suppressed"
944
+ ? disposition.priorityEmoji
945
+ : undefined;
923
946
  const reactionSuppressionEmoji =
924
- disposition.kind === "suppressed" ? disposition.emoji : undefined;
947
+ disposition.kind === "reaction-transition"
948
+ ? disposition.suppressionEmoji === undefined
949
+ ? item.reactionSuppressionEmoji
950
+ : disposition.suppressionEmoji ?? undefined
951
+ : disposition.kind === "suppressed"
952
+ ? disposition.emoji
953
+ : disposition.kind === "priority-suppressed"
954
+ ? disposition.suppressionEmoji
955
+ : undefined;
925
956
  if (
926
957
  item.queueLane === queueLane &&
927
958
  item.laneOrder === laneOrder &&
@@ -1984,7 +2015,6 @@ export interface TelegramSessionShutdownState<TQueueItem> {
1984
2015
  queuedTelegramItems: TQueueItem[];
1985
2016
  nextQueuedTelegramItemOrder: number;
1986
2017
  nextQueuedTelegramControlOrder: number;
1987
- nextPriorityReactionOrder: number;
1988
2018
  currentTelegramModel: undefined;
1989
2019
  activeTelegramToolExecutions: number;
1990
2020
  pendingTelegramModelSwitch: undefined;
@@ -1996,7 +2026,6 @@ export interface TelegramSessionShutdownState<TQueueItem> {
1996
2026
  export interface TelegramSessionRuntimeCounterState {
1997
2027
  nextQueuedTelegramItemOrder?: number;
1998
2028
  nextQueuedTelegramControlOrder?: number;
1999
- nextPriorityReactionOrder?: number;
2000
2029
  }
2001
2030
 
2002
2031
  export interface TelegramSessionRuntimeFlagState {
@@ -2104,8 +2133,7 @@ export interface TelegramQueueMutationRuntimeDeps<
2104
2133
  TContext,
2105
2134
  > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2106
2135
  ctx: TContext;
2107
- getNextPriorityReactionOrder?: () => number;
2108
- incrementNextPriorityReactionOrder?: () => void;
2136
+ allocateLaneOrder?: () => number;
2109
2137
  onItemsDiscarded?: (
2110
2138
  items: readonly TelegramQueueItem<TContext>[],
2111
2139
  ctx: TContext,
@@ -2116,8 +2144,7 @@ export interface TelegramQueueMutationRuntimeDeps<
2116
2144
  export interface TelegramQueueMutationControllerDeps<
2117
2145
  TContext,
2118
2146
  > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2119
- getNextPriorityReactionOrder?: () => number;
2120
- incrementNextPriorityReactionOrder?: () => void;
2147
+ allocateLaneOrder?: () => number;
2121
2148
  onItemsDiscarded?: (
2122
2149
  items: readonly TelegramQueueItem<TContext>[],
2123
2150
  ctx: TContext,
@@ -2227,7 +2254,6 @@ export function buildTelegramSessionShutdownState<
2227
2254
  queuedTelegramItems: [],
2228
2255
  nextQueuedTelegramItemOrder: 0,
2229
2256
  nextQueuedTelegramControlOrder: 0,
2230
- nextPriorityReactionOrder: 0,
2231
2257
  currentTelegramModel: undefined,
2232
2258
  activeTelegramToolExecutions: 0,
2233
2259
  pendingTelegramModelSwitch: undefined,
@@ -2492,24 +2518,40 @@ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
2492
2518
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
2493
2519
  scope?: TelegramQueueMessageScope,
2494
2520
  ): boolean {
2495
- const priorityLaneOrder =
2496
- disposition.kind === "priority"
2497
- ? deps.getNextPriorityReactionOrder?.()
2498
- : undefined;
2499
- if (disposition.kind === "priority" && priorityLaneOrder === undefined) {
2500
- return false;
2501
- }
2521
+ const queuedItems = deps.getQueuedItems();
2522
+ const changesLane = queuedItems.some((item) => {
2523
+ if (
2524
+ !isPendingTelegramTurn(item) ||
2525
+ !isTelegramQueueItemInMessageScope(item, scope) ||
2526
+ !item.sourceMessageIds.includes(messageId)
2527
+ ) {
2528
+ return false;
2529
+ }
2530
+ const queueLane: TelegramQueueLane =
2531
+ disposition.kind === "reaction-transition"
2532
+ ? disposition.priorityEmoji === undefined
2533
+ ? item.queueLane
2534
+ : disposition.priorityEmoji === null
2535
+ ? "default"
2536
+ : "priority"
2537
+ : disposition.kind === "priority" ||
2538
+ disposition.kind === "priority-suppressed"
2539
+ ? "priority"
2540
+ : "default";
2541
+ return item.queueLane !== queueLane;
2542
+ });
2543
+ const destinationLaneOrder = changesLane
2544
+ ? deps.allocateLaneOrder?.()
2545
+ : undefined;
2546
+ if (changesLane && destinationLaneOrder === undefined) return false;
2502
2547
  const { changed, items } = applyTelegramQueuePromptReactionDisposition(
2503
- deps.getQueuedItems(),
2548
+ queuedItems,
2504
2549
  messageId,
2505
2550
  disposition,
2506
- priorityLaneOrder,
2551
+ destinationLaneOrder,
2507
2552
  scope,
2508
2553
  );
2509
2554
  if (!changed) return false;
2510
- if (disposition.kind === "priority") {
2511
- deps.incrementNextPriorityReactionOrder?.();
2512
- }
2513
2555
  commitReorderedTelegramQueueItemsRuntime(items, deps);
2514
2556
  return true;
2515
2557
  }
@@ -2931,18 +2973,33 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
2931
2973
  { phase: "transport-generation" },
2932
2974
  );
2933
2975
  }
2934
- const dispatchableItems: TelegramQueueItem<TContext>[] = [];
2935
- const suppressedActiveItems: TelegramQueueItem<TContext>[] = [];
2936
- for (const item of activeItems) {
2937
- if (
2938
- item.kind === "prompt" &&
2939
- item.reactionSuppressionEmoji !== undefined
2940
- ) {
2941
- suppressedActiveItems.push(item);
2942
- } else {
2943
- dispatchableItems.push(item);
2976
+ const canDispatch = deps.canDispatch(ctx);
2977
+ let nextActiveIndex = 0;
2978
+ if (canDispatch) {
2979
+ while (nextActiveIndex < activeItems.length) {
2980
+ const candidate = activeItems[nextActiveIndex];
2981
+ if (
2982
+ !candidate ||
2983
+ candidate.kind !== "prompt" ||
2984
+ candidate.reactionSuppressionEmoji === undefined
2985
+ ) {
2986
+ break;
2987
+ }
2988
+ if (deps.hasPendingInboundQueueMutationForItem?.(candidate)) {
2989
+ deps.updateStatus(ctx);
2990
+ return;
2991
+ }
2992
+ if (
2993
+ deps.isQueueItemAdmissionReady &&
2994
+ !deps.isQueueItemAdmissionReady(candidate)
2995
+ ) {
2996
+ deps.updateStatus(ctx);
2997
+ return;
2998
+ }
2999
+ nextActiveIndex += 1;
2944
3000
  }
2945
3001
  }
3002
+ const dispatchableItems = activeItems.slice(nextActiveIndex);
2946
3003
  const nextItem = dispatchableItems[0];
2947
3004
  if (
2948
3005
  nextItem &&
@@ -2961,12 +3018,11 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
2961
3018
  }
2962
3019
  const dispatchPlan = planNextTelegramQueueAction(
2963
3020
  dispatchableItems,
2964
- deps.canDispatch(ctx),
3021
+ canDispatch,
2965
3022
  );
2966
- if (dispatchPlan.kind !== "none") {
3023
+ if (nextActiveIndex > 0 || dispatchPlan.kind !== "none") {
2967
3024
  deps.setQueuedItems([
2968
3025
  ...dispatchPlan.remainingItems,
2969
- ...suppressedActiveItems,
2970
3026
  ...protectedInactiveItems,
2971
3027
  ]);
2972
3028
  }
package/lib/routing.ts CHANGED
@@ -590,6 +590,11 @@ export interface TelegramInboundRouteRuntimeDeps<
590
590
  ctx: TContext,
591
591
  ) => Promise<boolean>;
592
592
  buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
593
+ invokeBoundButtonAction?: (
594
+ action: OutboundHandlers.TelegramOutboundButtonAction,
595
+ query: TCallbackQuery,
596
+ ctx: TContext,
597
+ ) => Promise<false | "new" | "edit">;
593
598
  inboundHandlerRuntime: TelegramInboundHandlerRuntime<TContext>;
594
599
  threadStore?: Threads.TelegramTopicTargetStore;
595
600
  updateStatus: (ctx: TContext, error?: string) => void;
@@ -1566,6 +1571,16 @@ export function createTelegramInboundRouteRuntime<
1566
1571
  {
1567
1572
  resolveAction: deps.buttonActionStore.resolve,
1568
1573
  answerCallbackQuery: deps.answerCallbackQuery,
1574
+ ...(deps.invokeBoundButtonAction
1575
+ ? {
1576
+ invokeBoundAction: (buttonQuery, action, context) =>
1577
+ deps.invokeBoundButtonAction!(
1578
+ action,
1579
+ buttonQuery as TCallbackQuery,
1580
+ context,
1581
+ ),
1582
+ }
1583
+ : {}),
1569
1584
  editMessageReplyMarkup: deps.editMessageReplyMarkup
1570
1585
  ? async (chatId, messageId, replyMarkup) => {
1571
1586
  try {
package/lib/runtime.ts CHANGED
@@ -10,7 +10,6 @@ const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
10
10
  export interface TelegramRuntimeQueueCounters {
11
11
  nextQueuedTelegramItemOrder: number;
12
12
  nextQueuedTelegramControlOrder: number;
13
- nextPriorityReactionOrder: number;
14
13
  }
15
14
 
16
15
  export interface TelegramRuntimeLifecycleFlags {
@@ -34,8 +33,6 @@ export interface TelegramRuntimeQueuePort {
34
33
  syncCounters: (counters: Partial<TelegramRuntimeQueueCounters>) => void;
35
34
  allocateItemOrder: () => number;
36
35
  allocateControlOrder: () => number;
37
- getNextPriorityReactionOrder: () => number;
38
- incrementNextPriorityReactionOrder: () => void;
39
36
  }
40
37
 
41
38
  export interface TelegramRuntimeLifecyclePort {
@@ -85,7 +82,6 @@ export function createTelegramBridgeRuntimeState(): TelegramBridgeRuntimeState {
85
82
  return {
86
83
  nextQueuedTelegramItemOrder: 0,
87
84
  nextQueuedTelegramControlOrder: 0,
88
- nextPriorityReactionOrder: 0,
89
85
  activeTelegramToolExecutions: 0,
90
86
  telegramTurnDispatchPending: false,
91
87
  compactionInProgress: false,
@@ -104,10 +100,6 @@ export function createTelegramBridgeRuntime(
104
100
  syncTelegramQueueRuntimeCounters(state, counters),
105
101
  allocateItemOrder: () => allocateTelegramQueueItemOrder(state),
106
102
  allocateControlOrder: () => allocateTelegramQueueControlOrder(state),
107
- getNextPriorityReactionOrder: () =>
108
- getNextTelegramPriorityReactionOrder(state),
109
- incrementNextPriorityReactionOrder: () =>
110
- incrementNextTelegramPriorityReactionOrder(state),
111
103
  },
112
104
  lifecycle: {
113
105
  syncFlags: (flags) => syncTelegramLifecycleRuntimeFlags(state, flags),
@@ -159,9 +151,6 @@ export function syncTelegramQueueRuntimeCounters(
159
151
  state.nextQueuedTelegramControlOrder =
160
152
  counters.nextQueuedTelegramControlOrder;
161
153
  }
162
- if (counters.nextPriorityReactionOrder !== undefined) {
163
- state.nextPriorityReactionOrder = counters.nextPriorityReactionOrder;
164
- }
165
154
  }
166
155
 
167
156
  export function allocateTelegramQueueItemOrder(
@@ -176,18 +165,6 @@ export function allocateTelegramQueueControlOrder(
176
165
  return state.nextQueuedTelegramControlOrder++;
177
166
  }
178
167
 
179
- export function getNextTelegramPriorityReactionOrder(
180
- state: TelegramBridgeRuntimeState,
181
- ): number {
182
- return state.nextPriorityReactionOrder;
183
- }
184
-
185
- export function incrementNextTelegramPriorityReactionOrder(
186
- state: TelegramBridgeRuntimeState,
187
- ): void {
188
- state.nextPriorityReactionOrder += 1;
189
- }
190
-
191
168
  export function syncTelegramLifecycleRuntimeFlags(
192
169
  state: TelegramBridgeRuntimeState,
193
170
  flags: Partial<TelegramRuntimeLifecycleFlags>,
package/lib/updates.ts CHANGED
@@ -133,24 +133,61 @@ export function getTelegramQueueReactionDisposition(
133
133
  emojis,
134
134
  TELEGRAM_REMOVAL_REACTION_EMOJIS,
135
135
  );
136
- if (suppressionEmoji) return { kind: "suppressed", emoji: suppressionEmoji };
137
136
  const priorityEmoji = getTelegramReactionEmoji(
138
137
  emojis,
139
138
  TELEGRAM_PRIORITY_REACTION_EMOJIS,
140
139
  );
140
+ if (suppressionEmoji && priorityEmoji) {
141
+ return {
142
+ kind: "priority-suppressed",
143
+ priorityEmoji,
144
+ suppressionEmoji,
145
+ };
146
+ }
147
+ if (suppressionEmoji) return { kind: "suppressed", emoji: suppressionEmoji };
141
148
  if (priorityEmoji) return { kind: "priority", emoji: priorityEmoji };
142
149
  return { kind: "default" };
143
150
  }
144
151
 
145
- function areTelegramQueueReactionDispositionsEqual(
146
- left: TelegramQueueReactionDisposition,
147
- right: TelegramQueueReactionDisposition,
148
- ): boolean {
149
- return (
150
- left.kind === right.kind &&
151
- (left.kind === "default" ||
152
- (right.kind !== "default" && left.emoji === right.emoji))
152
+ function getTelegramQueueReactionTransition(
153
+ oldReactions: TelegramReactionType[],
154
+ newReactions: TelegramReactionType[],
155
+ ): TelegramQueueReactionDisposition | undefined {
156
+ const oldEmojis = collectTelegramReactionEmojis(oldReactions);
157
+ const newEmojis = collectTelegramReactionEmojis(newReactions);
158
+ const oldPriorityEmoji = getTelegramReactionEmoji(
159
+ oldEmojis,
160
+ TELEGRAM_PRIORITY_REACTION_EMOJIS,
161
+ );
162
+ const newPriorityEmoji = getTelegramReactionEmoji(
163
+ newEmojis,
164
+ TELEGRAM_PRIORITY_REACTION_EMOJIS,
153
165
  );
166
+ const oldSuppressionEmoji = getTelegramReactionEmoji(
167
+ oldEmojis,
168
+ TELEGRAM_REMOVAL_REACTION_EMOJIS,
169
+ );
170
+ const newSuppressionEmoji = getTelegramReactionEmoji(
171
+ newEmojis,
172
+ TELEGRAM_REMOVAL_REACTION_EMOJIS,
173
+ );
174
+ if (
175
+ oldPriorityEmoji === newPriorityEmoji &&
176
+ oldSuppressionEmoji === newSuppressionEmoji
177
+ ) {
178
+ return undefined;
179
+ }
180
+ const transition: Extract<
181
+ TelegramQueueReactionDisposition,
182
+ { kind: "reaction-transition" }
183
+ > = { kind: "reaction-transition" };
184
+ if (oldPriorityEmoji !== newPriorityEmoji) {
185
+ transition.priorityEmoji = newPriorityEmoji ?? null;
186
+ }
187
+ if (oldSuppressionEmoji !== newSuppressionEmoji) {
188
+ transition.suppressionEmoji = newSuppressionEmoji ?? null;
189
+ }
190
+ return transition;
154
191
  }
155
192
 
156
193
  export function extractDeletedTelegramMessageIds(
@@ -1299,17 +1336,11 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
1299
1336
  typeof reactionUpdate.chat.id === "number"
1300
1337
  ? { chatId: reactionUpdate.chat.id }
1301
1338
  : undefined;
1302
- const oldDisposition = getTelegramQueueReactionDisposition(
1339
+ const reactionTransition = getTelegramQueueReactionTransition(
1303
1340
  reactionUpdate.old_reaction,
1304
- );
1305
- const newDisposition = getTelegramQueueReactionDisposition(
1306
1341
  reactionUpdate.new_reaction,
1307
1342
  );
1308
- if (
1309
- areTelegramQueueReactionDispositionsEqual(oldDisposition, newDisposition)
1310
- ) {
1311
- return;
1312
- }
1343
+ if (!reactionTransition) return;
1313
1344
  deps.assertExecutionCurrent?.();
1314
1345
  await deps.flushPendingMediaGroupMessage?.(reactionUpdate.message_id);
1315
1346
  deps.assertExecutionCurrent?.();
@@ -1317,7 +1348,7 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
1317
1348
  deps.assertExecutionCurrent?.();
1318
1349
  deps.applyQueuedTelegramTurnReactionByMessageId(
1319
1350
  reactionUpdate.message_id,
1320
- newDisposition,
1351
+ reactionTransition,
1321
1352
  deps.ctx,
1322
1353
  reactionScope,
1323
1354
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.35.1",
3
+ "version": "0.36.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,7 +23,9 @@ The primitive belongs to the Surface plane: it projects State, exposes Agency ca
23
23
 
24
24
  ## Scope
25
25
 
26
- Use this Skill only to synthesize a state-derived prompt-button surface. Operating or modifying an existing Telegram bridge menu, callback interface, fixed frontend, or runtime-owned control stays with that subsystem and does not route here merely because it contains buttons.
26
+ Use this Skill only to synthesize a state-derived prompt-button surface. Operating or modifying an existing Telegram bridge menu, callback interface, fixed frontend, runtime-owned control, or installed Generative App stays with that subsystem and does not route here merely because it contains buttons.
27
+
28
+ When a generated surface reveals a repeated stable interaction with bounded state and deterministic transitions, consider graduating it to the complementary `generative-apps` Skill. Keep one-off, interpretive, changing, and context-heavy interaction here; compile only when bypassing repeated model mediation has concrete latency, token, cost, reliability, or UX value.
27
29
 
28
30
  On Telegram turns, evaluate this Skill proactively rather than waiting for the user to ask for buttons. Load and apply it when a likely next decision, approval, navigation step, inspection, or bounded action can be made materially easier through controls; its correct output may still contain zero buttons when the admission test fails.
29
31
 
@@ -80,7 +82,9 @@ A surface normally contains:
80
82
 
81
83
  Prefer 2–6 controls for feedback and decisions; navigation collections may use up to 12 when the additional entries remain scannable. Split larger sets by category or page instead of building a button wall. Do not add navigation controls when the surface is a one-step decision.
82
84
 
83
- Present compact metadata as stacked key-value rows that reuse status-surface grammar: a short bold label, a colon, and an inline-code value when the value is path-like, numeric, an identifier, or machine state. Prefer ``**Path:** `/home/llb` `` and ``**Entries:** `1–10 of 52` `` on separate lines over prose fragments joined by a middle dot or other decorative section separator.
85
+ When a logical grid has no semantic column headings but Markdown table syntax requires a header row, use the grid's first data row as the syntactic header and render each remaining row once beneath it. Do not insert blank, dash-only, duplicate, or invented placeholder headings: they add a false row to the projected topology. Use ordinary semantic headings when the data actually has named columns.
86
+
87
+ Present compact metadata as stacked key-value rows that reuse status-surface grammar: a short bold label, a colon, and an inline-code value when the value is path-like, numeric, an identifier, or machine state. In Telegram Rich Markdown, use an actual Markdown list or blank paragraph boundaries so soft line breaks cannot collapse several fields into one visual line. Prefer ``- **Path:** `/home/llb` `` and ``- **Entries:** `1–10 of 52` `` over prose fragments joined by a middle dot or other decorative section separator.
84
88
 
85
89
  ## Truth Modes
86
90
 
@@ -118,7 +122,7 @@ Re-check mutable targets immediately before execution. Access denial never autho
118
122
 
119
123
  ## Prompt Buttons
120
124
 
121
- Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, a JSON matrix, or Compact Matrix Literal (CML). CML uses `{value}` or `{label|prompt}`, trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`; use JSON for multiline prompts, styles, or metadata. A top-level cell becomes one full-width row, while a nested row groups one or more controls horizontally without a parser-level width cap. Prefer one layout comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
125
+ Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, a JSON matrix, or Compact Matrix Literal (CML). CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires an explicit prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. Prefer CML whenever the model authors the control and it can express the required surface; fall back to expanded JSON only for multiline prompts, non-positional metadata, or a concrete CML parse/render failure, never merely from implementation habit. Deterministic Generative App scripts may return ordinary JSON because their source payload does not consume model-output tokens; author and operate those adapters through the `generative-apps` Skill rather than growing a parallel app workflow here. A top-level cell becomes one full-width row, while a nested row groups one or more controls horizontally without a parser-level width cap. Prefer one layout comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
122
126
 
123
127
  ### Semantic Row Composition
124
128
 
@@ -128,7 +132,9 @@ Model the control surface as an ordered ragged sequence of independently sized r
128
132
  - Use a singleton full-width row for a structurally independent, pinned, primary, summary, or high-consequence action when separation improves comprehension.
129
133
  - 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.
130
134
  - 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.
131
- - Use at most two columns when buttons carry words, phrases, icon-plus-text labels, or other text that must be read; move additional peer choices into more semantic rows rather than compressing four or more 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. 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.
135
+ - Use at most two columns when buttons carry words, phrases, icon-plus-text labels, or other text that must be read; move additional peer choices into more semantic rows rather than compressing four or more 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. 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.
136
+
137
+ 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.
132
138
 
133
139
  Treat symmetry as an evidence claim about the task. Equal rectangular rows imply equal relationships and stable spatial correspondence; do not make that claim merely because the renderer supports a grid. Infer the task's independent, peer, ordered, hierarchical, and spatial relationships first, then let those relationships determine row boundaries and widths. A non-spatial task should be ragged by default, and its asymmetry should remain visible when that best communicates hierarchy or action weight.
134
140
 
@@ -143,7 +149,7 @@ Use this catalog as a shape vocabulary, not a fixed menu of demos:
143
149
  - `1 → 2 → N×1`: Pinned structural navigation, compact traversal, then a vertical collection of independently readable items.
144
150
  - `1 → 2 → 3 → 1`: Ragged staged control where context, local alternatives, denser short peers, and a separated terminal action have different semantic weight.
145
151
  - Repeated `2`: Text-bearing choices distributed down the screen instead of compressed across it.
146
- - `R×C`: A rectangular layout only when rows and columns map to genuine coordinates, repeated dimensions, or another stable spatial topology.
152
+ - `R×C`: A rectangular layout only when rows and columns map to genuine coordinates, repeated dimensions, or another stable spatial topology; keep `C ≤ 8`, while `R` may be substantially larger when preserving vertical continuity is useful.
147
153
 
148
154
  Adapt a surface by identifying relationships, grouping only genuine peers, ordering groups by task hierarchy, applying label-pressure limits, and then choosing the least regular layout that remains immediately comprehensible. Do not select a catalog shape first and force the task into it.
149
155
 
@@ -156,6 +162,8 @@ Preserve the ordinary admission test: proactively offer an interactive surface e
156
162
  - Keep trivial interaction state in the visible conversation. When state becomes too large, long-lived, or error-prone for reliable conversational reconstruction, persist a small human-auditable Markdown state artifact at a deterministic task-owned path and render from it. The artifact belongs to the underlying task or domain, not to this Skill as shadow application state.
157
163
  - When transition rules are non-trivial or correctness-sensitive, use a small deterministic state-transition owner—script, module, tool, or existing domain API—that validates `current state + admitted action → next state`; let the model compile the surface from its result instead of informally simulating every transition. Do not create code or files for a trivial one-step interaction.
158
164
  - Treat repeated clicks against current state, not stale button appearance. If an action is already consumed or unavailable, keep state unchanged and say so briefly. Preserve an occupied or selected button when spatial layout matters, using its label or selected style as the visual state; omit unavailable controls when layout does not matter. Transport-level disabled buttons are optional, not assumed.
165
+ - Preserve tap-ahead on transports where existing controls remain actionable and rapid clicks queue separate turns. In a source-then-destination interaction, persist the source selection but do not regenerate the board, enumerate destinations, or duplicate controls between the two prompts; emit at most a minimal acknowledgement and let the already visible surface carry the destination click. Regenerate after the completed transition, invalid input, or evidence that the transport cannot preserve the intermediate surface.
166
+ - Resolve coordinate selection by current state rather than rigid click parity. Clicking any currently selectable source selects or replaces the source and then waits; acknowledge a replacement tersely without regenerating the surface. Only a click that is not a selectable source becomes a destination attempt when a source is already selected, at which point the domain owner validates the transition. Without a selected source, a non-source coordinate is a no-op.
159
167
 
160
168
  ```html
161
169
  <!-- telegram_button {"label":"🔍 Inspect run","prompt":"Inspect Run run:example read-only, summarize its current status and latest material evidence, then regenerate relevant supervision controls."} -->
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: generative-apps
3
+ description: Design, author, review, install, replace, invoke, or debug reusable Generative Apps that combine generated button interfaces, deterministic bound methods, and optional model-mediated prompts. Use for standalone applications and bounded view/controller adapters rendered through pi-telegram.
4
+ ---
5
+
6
+ # Generative Apps
7
+
8
+ ## Concept
9
+
10
+ A Generative App is a reusable application authored by the model for a concrete task. It combines persistent state, named methods, textual output, and a generated button interface. The model acts primarily as author/compiler; the installed program then renders evolving views and executes routine transitions without requiring inference on every click.
11
+
12
+ ```text
13
+ intent → model authors app → reusable state + methods + generated views
14
+ ```
15
+
16
+ One surface may deliberately mix two action planes:
17
+
18
+ ```text
19
+ bound method → deterministic local transition
20
+ ordinary prompt → model interpretation, explanation, teaching, or adaptation
21
+ ```
22
+
23
+ This is broader than one Telegram implementation. Telegram is the first renderer; a future TUI or web renderer may reuse the concept once a second implementation proves the common contract. Keep renderer-specific wire and lifecycle details with the owning runtime.
24
+
25
+ Generative Apps complement `generated-control-surface`:
26
+
27
+ ```text
28
+ Generated Control Surface → current context → model → one ephemeral surface
29
+ Generative App → model → reusable program → many evolving surfaces
30
+ ```
31
+
32
+ The `generated` / `generative` distinction is intentional. Do not rename `generated-control-surface` to a competing generative term.
33
+
34
+ ## Ownership
35
+
36
+ This Skill owns agent operating judgment:
37
+
38
+ - Whether a Generative App is warranted.
39
+ - Standalone versus adapter selection.
40
+ - Authoring and capability review.
41
+ - Install, replace, invoke, and validation workflow.
42
+ - Safety boundaries and stop decisions.
43
+
44
+ This Skill also owns the transport-independent concept, vocabulary, application shapes, hybrid action model, and relationship to Generated Control Surface.
45
+
46
+ [`../../docs/generative-apps.md`](../../docs/generative-apps.md) owns only the concrete `pi-telegram` runtime reference: Telegram wire syntax, managed layout, executable ABI, state timeline, generation/revision fencing, worker execution, locking, installation transactions, callback routing, lifecycle behavior, and current limitations. Do not duplicate those implementation details here.
47
+
48
+ ## When To Use
49
+
50
+ Use a Generative App when:
51
+
52
+ - Interaction repeats or contains several stable transitions.
53
+ - State and valid actions fit a small auditable contract.
54
+ - Direct methods materially improve latency, cost, reliability, or UX.
55
+ - Model judgment remains optional rather than required for every action.
56
+ - One clear owner exists for application or external domain state.
57
+
58
+ Prefer `generated-control-surface` for one-shot, interpretive, rapidly changing, or wholly model-mediated interaction. Do not create decorative apps, generic remote terminals, arbitrary command runners, or deterministic facades over ambiguous high-impact decisions.
59
+
60
+ ## Choose The Shape
61
+
62
+ ### Standalone deterministic application
63
+
64
+ The app owns a closed state machine such as a game, form, selector, simulation, or compact workflow. Its state must reconstruct the current view and explain the previous transition.
65
+
66
+ ### View/controller adapter
67
+
68
+ Another capability remains the authoritative real owner. The app stores validated adapter configuration and a last-observed projection only. Re-read the owner before mutation or explicit status; never promote cached view state into domain authority.
69
+
70
+ ## Authoring Workflow
71
+
72
+ 1. Identify the repeated feedback loop, real state owner, and actions that are truly deterministic.
73
+ 2. Choose one stable lowercase app and one self-contained `<app>.mjs` source outside the managed installation directory.
74
+ 3. Keep `init` and every exported method small, named, bounded, shell-free, and capability-specific.
75
+ 4. Render one complete next view after each action.
76
+ 5. Mix action planes intentionally:
77
+
78
+ ```text
79
+ app::method(argument) → bounded method without a model turn
80
+ ordinary prompt → model-mediated interpretation or explanation
81
+ ```
82
+
83
+ 6. Review state fields, arguments, process calls, rendered values, secrets, destructive effects, and failure paths.
84
+ 7. Install with `telegram_bind({ app, script, argument })`.
85
+ 8. Replace the same logical app only with explicit `replace: true`; never create `-v2` identities merely to reload code.
86
+ 9. Invoke read-only diagnostics with `telegram_bind({ app, method, argument, display: false })` when agent-side evidence is needed.
87
+ 10. Keep the maintained source with its capability owner; managed `genapps/` state is runtime installation, not source ownership.
88
+
89
+ ## Safety Rules
90
+
91
+ - A direct click authorizes only its exact installed method and validated JSON argument.
92
+ - Use exact executable plus argv through the bounded process port; never expose generic `exec` or shell text.
93
+ - Keep credentials and unrelated private state out of source, state, output, and diagnostics.
94
+ - Route consequences requiring contextual judgment through an ordinary model prompt.
95
+ - Fail closed on unavailable owners, stale actions, malformed state, absent methods, process failures, or uncertain effects.
96
+ - Do not claim automatic refresh, removal, voice output, or other behavior still marked incomplete in the runtime document.
97
+
98
+ ## Validation
99
+
100
+ Before presenting an app as working:
101
+
102
+ - Confirm app, source stem, installed identity, and bound prompts agree.
103
+ - Inspect the installed initial view and persisted bounded state.
104
+ - Exercise at least one real bound action and prove it bypasses Pi queue/model admission.
105
+ - Exercise at least one ordinary prompt when the app intentionally uses the model plane.
106
+ - Verify replacement rejects stale buttons and failed initialization preserves the prior app.
107
+ - For adapters, prove fresh external status and terminal mutation evidence.
108
+ - Confirm failures are bounded, redacted, and do not silently render success.
109
+
110
+ Stop and return to ordinary model interaction when the workflow cannot be represented safely as reviewed bounded methods plus explicit model prompts.
@@ -60,7 +60,7 @@ Button forms:
60
60
  <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
61
61
  ```
62
62
 
63
- - `telegram_button` accepts one JSON object, a JSON matrix, Compact Matrix Literal (CML), or double-quoted attributes; `telegram_buttons` is an exact plural alias. CML uses `{value}` or `{label|prompt}`, trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. In a matrix, each top-level cell becomes a full-width row and each nested row groups one or more buttons horizontally without a parser-level width cap. Prefer one matrix comment for multiple buttons, normally keep generated rows at five columns or fewer, and use six through eight only for short position-bearing labels. Keep the complete action in one top-level comment and encode multiline content with JSON `\n`.
63
+ - `telegram_button` accepts one JSON object, a JSON matrix, Compact Matrix Literal (CML), or double-quoted attributes; `telegram_buttons` is an exact plural alias. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires an explicit prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. Prefer CML for model-authored controls whenever it can express the required surface; use expanded JSON only for multiline prompts, non-positional metadata, or a concrete parse/render failure fallback, never merely from implementation habit. Deterministic Generative App scripts may use ordinary JSON because their output does not spend model tokens. In a matrix, each top-level cell becomes a full-width row and each nested row groups one or more buttons horizontally without a parser-level width cap. Prefer one matrix comment for multiple buttons, normally keep generated rows at five columns or fewer, and use six through eight only for short position-bearing labels. Keep the complete action in one top-level comment and encode multiline content with JSON `\n`.
64
64
  - Use `label` plus a self-contained `prompt`, or non-empty `value` when both are identical.
65
65
  - Optional `selected_style` is `primary` (default), `success`, or `danger`; style never suppresses prompt admission.
66
66
  - If button comments form the whole reply, the bridge supplies the standard choice heading.
@@ -102,6 +102,10 @@ Threaded Mode operates in private chats when Telegram exposes thread support for
102
102
 
103
103
  Cross-Thread delivery must preserve the concrete target and current registration authority. Use ordinary reply delivery for the source turn and `telegram_message(thread=...)` only for an explicitly requested different live Thread.
104
104
 
105
+ ## Generative Apps
106
+
107
+ Load and follow the bundled `generative-apps` Skill when work designs, authors, reviews, installs, replaces, invokes, or diagnoses a Generative App. Generative Apps compile stable repeated interaction into generated button views that may mix bound methods executed before Pi queue admission with ordinary model prompts; this bridge Skill continues to own Telegram transport, target authority, delivery, general button syntax, and turn behavior rather than duplicating the application workflow.
108
+
105
109
  ## Configurable Handlers And Extensions
106
110
 
107
111
  Prefer no-code command-template configuration in `telegram.json` before adding a companion extension: