@llblab/pi-kit 0.27.5 → 0.28.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.
Files changed (194) hide show
  1. package/BACKLOG.md +3 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +4 -4
  4. package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
  5. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +6 -2
  6. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -1
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +4 -3
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +26 -13
  9. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  10. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -1
  11. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
  12. package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -1
  13. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +25 -12
  15. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  16. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  17. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  18. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  19. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  20. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  21. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  22. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  23. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  25. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  26. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  27. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  28. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  29. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  30. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  31. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  32. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  33. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  34. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  35. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  36. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  37. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  38. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  39. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  40. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  41. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  42. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  43. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  44. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  45. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  46. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  47. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  48. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  49. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  50. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  51. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  52. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  53. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  54. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  55. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  56. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  57. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  58. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  59. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  60. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  61. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  62. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  63. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  64. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  66. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  67. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  68. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  69. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  70. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  71. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  72. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  73. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  74. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  75. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  76. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  77. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  78. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  79. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  81. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  82. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  83. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  84. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  86. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  87. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  88. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  89. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  90. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  91. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  92. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  93. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  94. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  95. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  97. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  98. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  99. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  100. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  101. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  102. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  104. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  105. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  106. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  108. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  109. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  110. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  111. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  112. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  113. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  114. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  115. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  116. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  117. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  118. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  119. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  120. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  121. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  122. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  123. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  124. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  125. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  126. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  127. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  128. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  129. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  130. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  131. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  132. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  133. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  134. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  135. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  136. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  137. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  138. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  139. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  140. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  141. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  142. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  143. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  144. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  145. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  146. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  147. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  148. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  149. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  150. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  151. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  152. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  153. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  154. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  155. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  156. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  157. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  158. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  159. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  160. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  161. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  162. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  163. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  164. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  165. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  166. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  167. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  168. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  169. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  170. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  171. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  172. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  173. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  174. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  175. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  176. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  177. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  178. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  179. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  180. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  181. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  182. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  183. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  184. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  185. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  186. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  187. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  188. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  189. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  190. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  191. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  192. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  193. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  194. package/package.json +3 -3
@@ -5,6 +5,7 @@
5
5
  * successor recovery, and fenced one-shot deletion before durable slot reuse.
6
6
  */
7
7
  import { isDeepStrictEqual } from "node:util";
8
+ import { areTelegramTargetsEqual as sameTarget } from "./target.js";
8
9
  import { getTelegramApiErrorRequestTarget, isTelegramApiRequestRejected, } from "./telegram-api.js";
9
10
  import { planTelegramWorkspaceSlotAllocation, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
10
11
  import { createTelegramWorkspaceAdmissionOperationId, isTelegramWorkspaceRetirementFence, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
@@ -55,7 +56,8 @@ export function captureTelegramWorkspaceJournalProtectionSources(input) {
55
56
  const snapshot = input.withJournalReference
56
57
  ? input.withJournalReference(binding, read)
57
58
  : read();
58
- sources.push({ kind: "available", scope, entries: snapshot.entries });
59
+ sources.push({ kind: "available", scope, entries: snapshot.entries,
60
+ ...(snapshot.exists === undefined ? {} : { exists: snapshot.exists }) });
59
61
  }
60
62
  catch {
61
63
  complete = false;
@@ -70,6 +72,10 @@ export function captureTelegramWorkspaceJournalProtectionSources(input) {
70
72
  journalBindingKey,
71
73
  }, input.createFollowerResolver(journalBindingKey));
72
74
  }
75
+ for (const source of input.binding.journalSources ?? []) {
76
+ capture({ kind: "binding", bindingKey: input.binding.bindingKey,
77
+ journalBindingKey: source.recipientBindingKey, sessionId: source.sessionId }, input.createSessionResolver?.(source.recipientBindingKey, source.sessionId) ?? (() => undefined));
78
+ }
73
79
  for (const path of input.discovery?.paths ?? []) {
74
80
  capture({ kind: "discovered", path }, input.discovery.createResolver(path));
75
81
  }
@@ -101,9 +107,6 @@ function getJournalUpdateTarget(update) {
101
107
  ...(typeof threadId === "number" ? { threadId } : {}),
102
108
  };
103
109
  }
104
- function sameTarget(left, right) {
105
- return left.chatId === right.chatId && left.threadId === right.threadId;
106
- }
107
110
  export function resolveTelegramWorkspaceAcceptedWorkProtection(input) {
108
111
  if (input.localAcceptedTargets.some((target) => sameTarget(target, input.binding.target))) {
109
112
  return "protected";
@@ -118,9 +121,17 @@ export function resolveTelegramWorkspaceAcceptedWorkProtection(input) {
118
121
  unknown = true;
119
122
  continue;
120
123
  }
124
+ if (input.requireBindingProvenance && source.scope.kind === "binding" && source.exists === false) {
125
+ unknown = true;
126
+ continue;
127
+ }
121
128
  if (source.scope.kind === "binding" && source.entries.length > 0) {
122
129
  return "protected";
123
130
  }
131
+ if (input.requireBindingProvenance && source.entries.length > 0) {
132
+ unknown = true;
133
+ continue;
134
+ }
124
135
  for (const entry of source.entries) {
125
136
  const target = getJournalUpdateTarget(entry.update);
126
137
  if (!target) {
@@ -144,49 +155,73 @@ export async function pruneTelegramWorkspaceJournalEvidence(input) {
144
155
  if (!isCurrent()) {
145
156
  throw new Error("Telegram Workspace journal pruning requires current leader authority.");
146
157
  }
147
- const shared = input.capture.sources.filter((source) => source.scope.kind === "shared");
148
- const bindingSources = input.capture.sources.filter((source) => source.scope.kind === "binding" &&
158
+ let capture;
159
+ try {
160
+ capture = input.capture();
161
+ }
162
+ catch {
163
+ return { kind: "blocked", reason: "incomplete-evidence" };
164
+ }
165
+ const address = (key, sessionId) => JSON.stringify([sessionId ?? null, key]);
166
+ const keys = input.binding.journalBindingKeys ?? [];
167
+ const sessions = input.binding.journalSources ?? [];
168
+ const expected = [
169
+ ...keys.map(key => address(key)),
170
+ ...sessions.map(source => address(source.recipientBindingKey, source.sessionId)),
171
+ ];
172
+ const shared = capture.sources.filter(source => source.scope.kind === "shared");
173
+ const bindingSources = capture.sources.filter(source => source.scope.kind === "binding" &&
149
174
  source.scope.bindingKey === input.binding.bindingKey);
150
- const sourceByKey = new Map(bindingSources.flatMap((source) => source.scope.kind === "binding"
151
- ? [[source.scope.journalBindingKey, source]]
152
- : []));
153
- if (!input.capture.complete ||
154
- shared.length !== 1 ||
155
- shared[0]?.kind !== "available" ||
156
- bindingSources.length !==
157
- (input.binding.journalBindingKeys ?? []).length ||
158
- sourceByKey.size !== (input.binding.journalBindingKeys ?? []).length ||
159
- Array.from(sourceByKey.values()).some((source) => source.kind !== "available")) {
175
+ const sourceByAddress = new Map(bindingSources.flatMap(source => source.scope.kind === "binding"
176
+ ? [[address(source.scope.journalBindingKey, source.scope.sessionId), source]] : []));
177
+ if (!capture.complete || shared.length !== 1 || shared[0]?.kind !== "available" ||
178
+ bindingSources.length !== expected.length || sourceByAddress.size !== expected.length ||
179
+ expected.some(key => sourceByAddress.get(key)?.kind !== "available")) {
160
180
  return { kind: "blocked", reason: "incomplete-evidence" };
161
181
  }
162
- const emptyKeys = (input.binding.journalBindingKeys ?? []).filter((key) => {
163
- const source = sourceByKey.get(key);
182
+ const empty = new Set(expected.filter(key => {
183
+ const source = sourceByAddress.get(key);
164
184
  return source?.kind === "available" && source.entries.length === 0;
165
- });
166
- if (emptyKeys.some((key) => input.getJournalWriterProtection(key) !== "clear")) {
185
+ }));
186
+ const emptyWriterKeys = new Set([
187
+ ...keys.filter(key => empty.has(address(key))),
188
+ ...sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)))
189
+ .map(source => source.recipientBindingKey),
190
+ ]);
191
+ const quiescent = new Set([...emptyWriterKeys].filter(key => input.getJournalWriterProtection(key) === "clear"));
192
+ const removedKeys = keys.filter(key => empty.has(address(key)) && quiescent.has(key));
193
+ const removedSources = sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)) &&
194
+ quiescent.has(source.recipientBindingKey));
195
+ if (empty.size > 0 && removedKeys.length === 0 && removedSources.length === 0) {
167
196
  return { kind: "blocked", reason: "writer-not-quiescent" };
168
197
  }
169
- const retainedKeys = (input.binding.journalBindingKeys ?? []).filter((key) => !emptyKeys.includes(key));
198
+ const retainedKeys = keys.filter(key => !removedKeys.includes(key));
199
+ const retainedSources = sessions.filter(source => !removedSources.includes(source));
170
200
  if (!isCurrent()) {
171
201
  throw new Error("Telegram Workspace journal pruning lost leader authority.");
172
202
  }
173
- const binding = input.store.commitWorkspaceJournalEvidence(input.binding, retainedKeys, input.binding.journalBindingsComplete === true);
203
+ const binding = input.store.commitWorkspaceJournalEvidence(input.binding, retainedKeys, input.binding.journalBindingsComplete === true, retainedSources);
174
204
  if (!binding)
175
205
  return { kind: "blocked", reason: "state-changed" };
176
- const removedKeys = (input.binding.journalBindingKeys ?? []).filter((key) => !retainedKeys.includes(key));
177
- if (removedKeys.length > 0)
178
- await input.store.persist();
206
+ if (removedKeys.length > 0 || removedSources.length > 0) {
207
+ const published = await input.store.persistWorkspaceJournalEvidence(binding, isCurrent);
208
+ if (!isCurrent())
209
+ throw new Error("Telegram Workspace journal pruning lost leader authority.");
210
+ if (!published)
211
+ return { kind: "blocked", reason: "publication-refused" };
212
+ }
179
213
  if (!isCurrent()) {
180
214
  throw new Error("Telegram Workspace journal pruning lost leader authority.");
181
215
  }
182
- return { kind: "committed", binding, removedKeys };
216
+ return { kind: "committed", binding, removedKeys,
217
+ ...(removedSources.length ? { removedSources: removedSources.map(source => ({ ...source })) } : {}) };
183
218
  };
184
219
  return runWithTelegramWorkspaceAdmissionsAsync({
185
220
  ledger: input.admission,
186
221
  operationId: createTelegramWorkspaceAdmissionOperationId(),
187
222
  operationKind: "workspace.prune-journal-evidence",
188
223
  scopes: [{ kind: "target", target: input.binding.target }],
189
- operation,
224
+ operation: () => input.runExclusive(operation),
190
225
  onReleaseError(error) {
191
226
  input.onAdmissionReleaseError?.(error);
192
227
  },
@@ -210,6 +245,7 @@ export function captureTelegramWorkspaceExternalProtection(input) {
210
245
  localAcceptedTargets: local.targets,
211
246
  journalSources: journals.sources,
212
247
  sourcesComplete: journals.complete && local.complete,
248
+ requireBindingProvenance: input.requireBindingProvenance,
213
249
  });
214
250
  }
215
251
  catch {
@@ -223,17 +259,45 @@ export function captureTelegramWorkspaceExternalProtection(input) {
223
259
  }
224
260
  return { liveOwner, acceptedWork, deliveryAuthority };
225
261
  }
226
- export function createTelegramWorkspaceExternalProtectionCapture(deps) {
227
- return function (binding) {
262
+ /** Shared read-only observer: protection and metadata pruning use the same scoped evidence path. */
263
+ export function createTelegramWorkspaceProtectionObserver(deps) {
264
+ const captureJournalSources = (candidate) => {
265
+ const namespace = deps.inspectJournalNamespace?.();
266
+ if (deps.inspectJournalNamespace && (!namespace || namespace.sources.filter(source => source.role === "polling").length !== 1))
267
+ throw new Error("Telegram strict namespace evidence is unavailable or incomplete.");
268
+ if (namespace?.retainedInputs?.some(original => original.state !== "committed"))
269
+ throw new Error("Telegram namespace contains uncommitted private retention evidence.");
270
+ const discovery = namespace
271
+ ? { paths: namespace.sources.filter(source => source.role !== "polling" && source.evidence.kind === "present")
272
+ .map(source => source.path), complete: true }
273
+ : candidate.journalBindingsComplete === true ? undefined : deps.discoverFollowerJournals?.();
274
+ if (namespace && !deps.createJournalPathResolver)
275
+ throw new Error("Telegram strict namespace protection requires discovered source resolution.");
276
+ return captureTelegramWorkspaceJournalProtectionSources({
277
+ binding: candidate,
278
+ resolveLeader: deps.resolveLeaderJournal,
279
+ createFollowerResolver: deps.createFollowerJournalResolver,
280
+ ...(deps.createSessionJournalResolver ? { createSessionResolver: deps.createSessionJournalResolver } : {}),
281
+ ...(deps.withJournalReference ? { withJournalReference: deps.withJournalReference } : {}),
282
+ ...(discovery && deps.createJournalPathResolver
283
+ ? { discovery: { ...discovery, createResolver: deps.createJournalPathResolver } } : {}),
284
+ });
285
+ };
286
+ const capture = function (binding, options) {
228
287
  return captureTelegramWorkspaceExternalProtection({
229
288
  binding,
289
+ requireBindingProvenance: options?.requireBindingProvenance,
230
290
  getLiveOwnerProtection(candidate) {
231
291
  if (deps.listFollowers().some((follower) => !!follower.target && sameTarget(follower.target, candidate.target)))
232
292
  return "protected";
233
293
  if (!deps.getJournalWriterProtection)
234
294
  return "unknown";
235
295
  let unknown = candidate.journalBindingsComplete !== true;
236
- for (const journalBindingKey of candidate.journalBindingKeys ?? []) {
296
+ const writerKeys = new Set([
297
+ ...(candidate.journalBindingKeys ?? []),
298
+ ...(candidate.journalSources ?? []).map(source => source.recipientBindingKey),
299
+ ]);
300
+ for (const journalBindingKey of writerKeys) {
237
301
  const protection = deps.getJournalWriterProtection(journalBindingKey);
238
302
  if (protection === "protected")
239
303
  return "protected";
@@ -257,29 +321,34 @@ export function createTelegramWorkspaceExternalProtectionCapture(deps) {
257
321
  }
258
322
  return { targets, complete };
259
323
  },
260
- captureJournalSources(candidate) {
261
- const discovery = candidate.journalBindingsComplete === true
262
- ? undefined
263
- : deps.discoverFollowerJournals?.();
264
- return captureTelegramWorkspaceJournalProtectionSources({
265
- binding: candidate,
266
- resolveLeader: deps.resolveLeaderJournal,
267
- createFollowerResolver: deps.createFollowerJournalResolver,
268
- ...(deps.withJournalReference
269
- ? { withJournalReference: deps.withJournalReference } : {}),
270
- ...(discovery && deps.createJournalPathResolver
271
- ? { discovery: {
272
- ...discovery,
273
- createResolver: deps.createJournalPathResolver,
274
- } }
275
- : {}),
276
- });
277
- },
324
+ captureJournalSources,
278
325
  ...(deps.getDeliveryAuthorityProtection
279
326
  ? { getDeliveryAuthorityProtection: deps.getDeliveryAuthorityProtection }
280
327
  : {}),
281
328
  });
282
329
  };
330
+ return { capture, captureJournalSources,
331
+ getJournalWriterProtection: deps.getJournalWriterProtection ?? (() => "unknown") };
332
+ }
333
+ /** Callable protection view retained for callers that need no metadata observation ports. */
334
+ export function createTelegramWorkspaceExternalProtectionCapture(deps) {
335
+ return createTelegramWorkspaceProtectionObserver(deps).capture;
336
+ }
337
+ export function createTelegramWorkspaceJournalEvidencePruner(deps) {
338
+ return async (binding, isCurrent) => {
339
+ const admission = deps.getAdmission();
340
+ if (!admission)
341
+ return { kind: "blocked", reason: "state-changed" };
342
+ const profileKey = admission.getProfileKey();
343
+ return pruneTelegramWorkspaceJournalEvidence({
344
+ store: deps.store, binding, admission, runExclusive: deps.runExclusive,
345
+ capture: () => deps.protection.captureJournalSources(binding),
346
+ getJournalWriterProtection: deps.protection.getJournalWriterProtection,
347
+ getLeaderEpoch: deps.getLeaderEpoch,
348
+ getProfileKey: () => deps.getAdmission()?.getProfileKey() ?? "",
349
+ isCurrent: () => isCurrent() && deps.getAdmission()?.getProfileKey() === profileKey,
350
+ });
351
+ };
283
352
  }
284
353
  export function isCurrentTelegramWorkspaceBinding(store, expected) {
285
354
  return store.listWorkspaceBindings().some((binding) => isDeepStrictEqual(binding, expected));
@@ -307,6 +376,9 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
307
376
  if (!isCurrent() || !deps.isBindingCurrent(binding)) {
308
377
  return { kind: "blocked", reason: "authority-changed" };
309
378
  }
379
+ // Session addresses require exact resolution; never substitute a flat same-hash journal.
380
+ if (binding.journalSources?.length && !deps.createSessionJournalResolver)
381
+ return { kind: "blocked", reason: "incomplete-source" };
310
382
  const initial = deps.getExternalProtection(binding);
311
383
  if (initial.liveOwner !== "clear" || initial.deliveryAuthority !== "clear") {
312
384
  return { kind: "blocked", reason: "live-owner" };
@@ -345,6 +417,9 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
345
417
  for (const key of binding.journalBindingKeys ?? []) {
346
418
  capture("binding", deps.createFollowerJournalResolver(key));
347
419
  }
420
+ for (const source of binding.journalSources ?? []) {
421
+ capture("binding", deps.createSessionJournalResolver(source.recipientBindingKey, source.sessionId));
422
+ }
348
423
  for (const path of discovery?.paths ?? []) {
349
424
  if (!deps.createJournalPathResolver) {
350
425
  complete = false;
@@ -957,21 +1032,42 @@ export function createTelegramWorkspaceSlotRotation(input) {
957
1032
  catch (error) {
958
1033
  if (!(error instanceof TelegramWorkspaceSlotUnavailableError))
959
1034
  throw error;
960
- if (input.reclaimDeadOwnerQueuedWork) {
1035
+ if (input.pruneJournalEvidence || input.reclaimDeadOwnerQueuedWork) {
961
1036
  const candidates = await input.runExclusive(async () => {
962
1037
  await input.store.load();
963
1038
  if (!isCurrent())
964
1039
  throw new Error("Telegram Workspace reclamation lost leader authority.");
965
1040
  const snapshot = input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection);
966
1041
  const allocation = planTelegramWorkspaceSlotAllocation({ ...snapshot, nowMs: Date.now() });
967
- if (allocation.kind !== "blocked" || allocation.reason !== "protected-capacity")
1042
+ if (allocation.kind === "free" || (allocation.kind === "blocked" && allocation.reason !== "protected-capacity"))
1043
+ return [];
1044
+ if (allocation.kind === "reclaim" && !input.pruneJournalEvidence)
968
1045
  return [];
969
- return input.store.listWorkspaceBindings().filter((binding) => typeof binding.inactiveSinceMs === "number" &&
1046
+ return input.store.listWorkspaceBindings().filter((binding) => (allocation.kind !== "reclaim" || binding.bindingKey === allocation.candidate.bindingKey) &&
1047
+ typeof binding.inactiveSinceMs === "number" &&
970
1048
  Number.isFinite(binding.inactiveSinceMs) &&
971
1049
  binding.inactiveSinceMs >= 0).sort((left, right) => left.inactiveSinceMs - right.inactiveSinceMs ||
972
1050
  (left.slot ?? "").localeCompare(right.slot ?? ""));
973
1051
  });
974
- for (const binding of candidates) {
1052
+ for (const original of candidates) {
1053
+ let binding = original;
1054
+ if (input.pruneJournalEvidence && ((binding.journalBindingKeys?.length ?? 0) + (binding.journalSources?.length ?? 0) > 0)) {
1055
+ const pruned = await input.pruneJournalEvidence(binding, isCurrent);
1056
+ if (!isCurrent())
1057
+ throw new Error("Telegram Workspace pruning lost leader authority.");
1058
+ if (pruned.kind !== "committed")
1059
+ continue;
1060
+ binding = pruned.binding;
1061
+ const removed = pruned.removedKeys.length + (pruned.removedSources?.length ?? 0);
1062
+ if (removed > 0) {
1063
+ try {
1064
+ input.recordEvent("Telegram Workspace journal evidence pruned.", { slot: binding.slot, removed });
1065
+ }
1066
+ catch { /* Diagnostics cannot revoke a published metadata subset. */ }
1067
+ }
1068
+ }
1069
+ if (!input.reclaimDeadOwnerQueuedWork)
1070
+ continue;
975
1071
  const evidence = input.getExternalProtection(binding);
976
1072
  if (evidence.liveOwner !== "clear" ||
977
1073
  evidence.acceptedWork !== "protected" ||
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.51.6",
3
+ "version": "0.52.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -27,8 +27,8 @@
27
27
  "node": ">=22.19.0"
28
28
  },
29
29
  "scripts": {
30
- "test": "node --experimental-strip-types --test --test-concurrency=4 --test-reporter=dot tests/*.test.ts",
31
- "test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
30
+ "test": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-concurrency=4 --test-reporter=dot tests/*.test.ts",
31
+ "test:verbose": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-reporter=spec tests/*.test.ts",
32
32
  "typecheck": "tsc --noEmit",
33
33
  "build": "node scripts/build-dist.mjs",
34
34
  "build:check": "node scripts/build-dist.mjs --check",
@@ -109,9 +109,9 @@
109
109
  "image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
110
110
  },
111
111
  "peerDependencies": {
112
- "@earendil-works/pi-agent-core": ">=0.84.4",
113
- "@earendil-works/pi-ai": ">=0.84.4",
114
- "@earendil-works/pi-coding-agent": ">=0.84.4",
112
+ "@earendil-works/pi-agent-core": ">=1.0.0",
113
+ "@earendil-works/pi-ai": ">=1.0.0",
114
+ "@earendil-works/pi-coding-agent": ">=1.0.0",
115
115
  "@sinclair/typebox": "*"
116
116
  },
117
117
  "devDependencies": {
@@ -46,7 +46,7 @@ A disabled control is not an action: it needs no prompt or selected style and mu
46
46
 
47
47
  Prompts must name any target, operation, constraint, or freshness identity whose omission could change the action. Reuse visible context only when it remains unambiguous under delayed or reordered clicks. Never encode volatile output that should be freshly inspected.
48
48
 
49
- Every generated human-readable action label must use `emoji + space + text`; emoji-free text labels are invalid. Choose the emoji by action semantics, keep its meaning consistent, and never rely on emoji or color alone. Coordinates, established symbolic controls, and intentionally spatial glyphs already satisfy the marker role through their domain grammar. If prompt buttons are unavailable, preserve the same choices as a numbered list.
49
+ Every generated human-readable action label must use `emoji + space + text`; emoji-free text labels are invalid. Choose the emoji by action semantics, keep its meaning consistent, and never rely on emoji or color alone. State/value controls follow their UI-kit grammar; unmarked inactive list items are not action labels. Coordinates, established symbolic controls, and intentionally spatial glyphs already satisfy the marker role through their domain grammar. If prompt buttons are unavailable, preserve the same choices as a numbered list.
50
50
 
51
51
  ## Layout Kernel
52
52
 
@@ -97,10 +97,10 @@ Show concise failure evidence and emit valid recovery controls such as diagnose,
97
97
  Before sending:
98
98
 
99
99
  - At least one useful button is present whenever the transport supports prompt buttons.
100
- - Every human-readable button label begins with a semantic emoji and one ASCII space.
100
+ - Every human-readable action label begins with a semantic emoji and one ASCII space; state/value controls follow their owning documented grammar.
101
101
  - State, actions, owner, and target agree.
102
102
  - Live claims are fresh; adaptation and incompleteness are explicit.
103
- - Every prompt is sufficient and every label follows its semantic emoji rule.
103
+ - Every prompt is sufficient; labels follow their control grammar.
104
104
  - Row grouping reflects real hierarchy or peer relationships.
105
105
  - High-impact actions require confirmation.
106
106
  - No secret or hidden reasoning appears.
@@ -6,12 +6,12 @@ Inspect in this order:
6
6
 
7
7
  1. `/telegram-status` for compact health in the Pi TUI.
8
8
  2. `/telegram-status --debug` for bounded human-readable diagnostics in the Pi TUI.
9
- 3. `~/.pi/agent/tmp/telegram/state.json` and `logs.jsonl` for default-profile redacted evidence.
10
- 4. `state.<profile>.json` and `logs.<profile>.jsonl` for a named profile.
9
+ 3. `~/.pi/agent/tmp/pi-telegram/state.json` (per-profile `transport`, `workspace` canonical bindings and `runtime` roster/diagnostics sections) and `logs.jsonl` (profile-labelled redacted evidence); rotated history is `logs/logs._prev.jsonl`.
10
+ 4. For a named profile, read its `profiles.<name>` entry and filter log records by `profile`; service journals are `journals/<kind>.<profile sha256>.json`.
11
11
 
12
12
  These slash commands are registered Pi commands, not shell executables or agent tools. If the agent cannot invoke them through a supported Pi surface, read the diagnostic files directly; do not run them in Bash or inject terminal input.
13
13
 
14
- When `PI_CODING_AGENT_DIR` selects another compatible runtime, resolve its equivalent `tmp/telegram` directory.
14
+ When `PI_CODING_AGENT_DIR` selects another compatible runtime, resolve its equivalent `tmp/pi-telegram` directory.
15
15
 
16
16
  A `persistent-conflict` polling stop means the bounded competing-`getUpdates` threshold triggered full transport stand-down, not cancellation of accepted local Pi work. The terminal diagnostic distinguishes lost local ownership from a competing client despite an apparently owned lock. Check other profiles, agent directories, installations, or non-Pi clients sharing the bot; after removing the competition, reconnect through the supported Pi command. Do not restart repeatedly or alter lock files to compete for the stream.
17
17
 
@@ -22,7 +22,7 @@ Living index of project documentation in `/docs`.
22
22
  - [generative-apps.md](./generative-apps.md) — Generative Apps runtime and wire contract for managed `.mjs` identity, inference-bypass bindings, persistent state timelines, bounded adapters, replacement, and lifecycle; agent operation lives in the bundled `generative-apps` Skill
23
23
  - [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
24
24
  - [updates.md](./updates.md) — Update classification and runtime handler registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
25
- - [multi-instance-bus.md](./multi-instance-bus.md) — Optional multi-instance Telegram bus architecture: profile-scoped transport, leader/follower routing, thread targets, instance slots, manual follower registration, and recovery semantics
25
+ - [multi-instance-bus.md](./multi-instance-bus.md) — Optional multi-instance Telegram bus architecture: profile-scoped transport, leader/follower routing, thread targets, instance slots, session-owned journal storage and sweeping, manual follower registration, and recovery semantics
26
26
  - [sections.md](./sections.md) — Telegram Extension Sections Standard: registration contract, context ports, callback routing, navigation hierarchy, and demo reference for pi extensions that want Telegram UI surfaces
27
27
  - [voice.md](./voice.md) — Voice integration guide: detection, reply policy, STT/TTS provider registration, provider-owned conversion, and transparent interception
28
28
  - [ui-style.md](./ui-style.md) — Inline UI style guide for buttons, toggles, tabs, option lists, cards, and dialogs
@@ -14,7 +14,7 @@ import {
14
14
  } from "@llblab/pi-telegram/activity";
15
15
  ```
16
16
 
17
- An issue #126 consumer can own optional Settings policy for reasoning, intermediate assistant prose, and tool rows. Those visibility choices do not become mandatory bridge-core settings.
17
+ An external consumer can own optional Settings policy for reasoning, intermediate assistant prose, and tool rows. Those visibility choices do not become mandatory bridge-core settings.
18
18
 
19
19
  ## Ownership Boundary
20
20