@llblab/pi-telegram 0.51.5 → 0.52.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 (181) hide show
  1. package/AGENTS.md +10 -8
  2. package/BACKLOG.md +1 -8
  3. package/CHANGELOG.md +23 -0
  4. package/LICENSE +21 -0
  5. package/README.md +16 -6
  6. package/dist/lib/activity-verbosity.d.ts +1 -5
  7. package/dist/lib/activity-verbosity.js +5 -7
  8. package/dist/lib/bindings.d.ts +9 -2
  9. package/dist/lib/bindings.js +22 -13
  10. package/dist/lib/bus-api.js +4 -2
  11. package/dist/lib/bus-follower.d.ts +55 -57
  12. package/dist/lib/bus-follower.js +213 -140
  13. package/dist/lib/bus-leader.d.ts +21 -2
  14. package/dist/lib/bus-leader.js +181 -25
  15. package/dist/lib/bus-transport.d.ts +4 -1
  16. package/dist/lib/bus-transport.js +28 -4
  17. package/dist/lib/bus.d.ts +52 -56
  18. package/dist/lib/bus.js +78 -246
  19. package/dist/lib/channel-posts.d.ts +1 -3
  20. package/dist/lib/channel-posts.js +23 -26
  21. package/dist/lib/command-templates.d.ts +0 -5
  22. package/dist/lib/command-templates.js +5 -5
  23. package/dist/lib/commands.d.ts +12 -26
  24. package/dist/lib/commands.js +152 -98
  25. package/dist/lib/config.d.ts +0 -17
  26. package/dist/lib/config.js +12 -14
  27. package/dist/lib/delivery.d.ts +0 -4
  28. package/dist/lib/delivery.js +2 -5
  29. package/dist/lib/extension.js +281 -61
  30. package/dist/lib/generative-apps.d.ts +0 -19
  31. package/dist/lib/inbound.d.ts +0 -2
  32. package/dist/lib/inbound.js +2 -2
  33. package/dist/lib/journal.d.ts +177 -16
  34. package/dist/lib/journal.js +1115 -249
  35. package/dist/lib/keyboard.d.ts +0 -2
  36. package/dist/lib/keyboard.js +2 -2
  37. package/dist/lib/lifecycle.d.ts +57 -2
  38. package/dist/lib/lifecycle.js +110 -5
  39. package/dist/lib/locks.d.ts +100 -12
  40. package/dist/lib/locks.js +380 -14
  41. package/dist/lib/logging.d.ts +30 -12
  42. package/dist/lib/logging.js +129 -72
  43. package/dist/lib/media.d.ts +28 -5
  44. package/dist/lib/media.js +26 -8
  45. package/dist/lib/menu-model.d.ts +0 -11
  46. package/dist/lib/menu-model.js +8 -8
  47. package/dist/lib/menu-settings.d.ts +0 -18
  48. package/dist/lib/menu-settings.js +18 -18
  49. package/dist/lib/menu-status.d.ts +4 -4
  50. package/dist/lib/menu-status.js +12 -7
  51. package/dist/lib/menu-thinking.d.ts +1 -1
  52. package/dist/lib/menu-thinking.js +1 -1
  53. package/dist/lib/menu.d.ts +1 -0
  54. package/dist/lib/menu.js +4 -3
  55. package/dist/lib/model.js +2 -4
  56. package/dist/lib/outbound-attachments.d.ts +0 -1
  57. package/dist/lib/outbound-attachments.js +1 -1
  58. package/dist/lib/outbound-buttons.js +1 -8
  59. package/dist/lib/outbound-markup.d.ts +1 -6
  60. package/dist/lib/outbound-markup.js +4 -21
  61. package/dist/lib/outbound.d.ts +0 -8
  62. package/dist/lib/outbound.js +5 -5
  63. package/dist/lib/paths.d.ts +50 -7
  64. package/dist/lib/paths.js +155 -17
  65. package/dist/lib/pi.d.ts +1 -0
  66. package/dist/lib/pi.js +2 -1
  67. package/dist/lib/polling.d.ts +0 -5
  68. package/dist/lib/polling.js +5 -5
  69. package/dist/lib/preview.d.ts +0 -3
  70. package/dist/lib/preview.js +3 -3
  71. package/dist/lib/prompt-templates.d.ts +0 -1
  72. package/dist/lib/prompt-templates.js +1 -1
  73. package/dist/lib/queue.d.ts +3 -15
  74. package/dist/lib/queue.js +69 -8
  75. package/dist/lib/recovery.d.ts +29 -9
  76. package/dist/lib/recovery.js +104 -33
  77. package/dist/lib/replies.d.ts +0 -4
  78. package/dist/lib/replies.js +1 -1
  79. package/dist/lib/routing.d.ts +73 -8
  80. package/dist/lib/routing.js +2206 -307
  81. package/dist/lib/sections.d.ts +0 -2
  82. package/dist/lib/sections.js +1 -1
  83. package/dist/lib/status.d.ts +46 -9
  84. package/dist/lib/status.js +179 -27
  85. package/dist/lib/sync.d.ts +0 -5
  86. package/dist/lib/sync.js +9 -8
  87. package/dist/lib/telegram-api.d.ts +11 -3
  88. package/dist/lib/telegram-api.js +34 -23
  89. package/dist/lib/thread-cleanup-manager.d.ts +1 -0
  90. package/dist/lib/thread-cleanup-manager.js +18 -21
  91. package/dist/lib/thread-naming.d.ts +32 -5
  92. package/dist/lib/thread-naming.js +191 -7
  93. package/dist/lib/thread-reconciler.d.ts +2 -0
  94. package/dist/lib/thread-reconciler.js +1 -1
  95. package/dist/lib/threads.d.ts +292 -59
  96. package/dist/lib/threads.js +1664 -373
  97. package/dist/lib/turns.d.ts +0 -1
  98. package/dist/lib/turns.js +1 -1
  99. package/dist/lib/updates.d.ts +184 -28
  100. package/dist/lib/updates.js +1032 -114
  101. package/dist/lib/wire.d.ts +12 -0
  102. package/dist/lib/wire.js +21 -0
  103. package/dist/lib/workspace-admission.d.ts +25 -17
  104. package/dist/lib/workspace-admission.js +95 -20
  105. package/dist/lib/workspace-identity.d.ts +20 -0
  106. package/dist/lib/workspace-identity.js +103 -0
  107. package/dist/lib/workspace-retirement.d.ts +45 -10
  108. package/dist/lib/workspace-retirement.js +196 -64
  109. package/dist/package.json +6 -6
  110. package/dist/skills/generated-control-surface/SKILL.md +3 -3
  111. package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  112. package/docs/README.md +1 -1
  113. package/docs/activity.md +1 -1
  114. package/docs/architecture.md +158 -39
  115. package/docs/callback-namespaces.md +1 -1
  116. package/docs/delivery.md +1 -1
  117. package/docs/generative-apps.md +2 -2
  118. package/docs/inbound.md +1 -1
  119. package/docs/multi-instance-bus.md +127 -19
  120. package/docs/public-api.md +5 -3
  121. package/docs/ui-style.md +26 -8
  122. package/docs/updates.md +1 -1
  123. package/lib/activity-verbosity.ts +5 -9
  124. package/lib/bindings.ts +23 -10
  125. package/lib/bus-api.ts +5 -2
  126. package/lib/bus-follower.ts +251 -226
  127. package/lib/bus-leader.ts +178 -31
  128. package/lib/bus-transport.ts +30 -8
  129. package/lib/bus.ts +110 -334
  130. package/lib/channel-posts.ts +29 -29
  131. package/lib/command-templates.ts +5 -5
  132. package/lib/commands.ts +150 -112
  133. package/lib/config.ts +12 -17
  134. package/lib/delivery.ts +2 -8
  135. package/lib/extension.ts +282 -72
  136. package/lib/generative-apps.ts +0 -22
  137. package/lib/inbound.ts +2 -2
  138. package/lib/journal.ts +1182 -326
  139. package/lib/keyboard.ts +2 -2
  140. package/lib/lifecycle.ts +141 -5
  141. package/lib/locks.ts +402 -23
  142. package/lib/logging.ts +151 -101
  143. package/lib/media.ts +51 -9
  144. package/lib/menu-model.ts +8 -8
  145. package/lib/menu-settings.ts +18 -18
  146. package/lib/menu-status.ts +11 -0
  147. package/lib/menu-thinking.ts +2 -2
  148. package/lib/menu.ts +5 -1
  149. package/lib/model.ts +2 -4
  150. package/lib/outbound-attachments.ts +1 -1
  151. package/lib/outbound-buttons.ts +1 -10
  152. package/lib/outbound-markup.ts +5 -24
  153. package/lib/outbound.ts +5 -5
  154. package/lib/paths.ts +177 -17
  155. package/lib/pi.ts +5 -2
  156. package/lib/polling.ts +5 -5
  157. package/lib/preview.ts +3 -3
  158. package/lib/prompt-templates.ts +1 -1
  159. package/lib/queue.ts +67 -9
  160. package/lib/recovery.ts +104 -44
  161. package/lib/replies.ts +1 -1
  162. package/lib/routing.ts +1955 -375
  163. package/lib/sections.ts +1 -1
  164. package/lib/status.ts +185 -36
  165. package/lib/sync.ts +8 -8
  166. package/lib/telegram-api.ts +48 -33
  167. package/lib/thread-cleanup-manager.ts +24 -21
  168. package/lib/thread-naming.ts +267 -9
  169. package/lib/thread-reconciler.ts +3 -1
  170. package/lib/threads.ts +1718 -491
  171. package/lib/turns.ts +1 -1
  172. package/lib/updates.ts +1072 -151
  173. package/lib/wire.ts +28 -0
  174. package/lib/workspace-admission.ts +97 -47
  175. package/lib/workspace-identity.ts +147 -0
  176. package/lib/workspace-retirement.ts +234 -102
  177. package/package.json +6 -6
  178. package/scripts/audit-exports.mjs +100 -0
  179. package/scripts/check-downgrade.mjs +80 -43
  180. package/skills/generated-control-surface/SKILL.md +3 -3
  181. package/skills/telegram-bridge/references/diagnosis.md +3 -3
@@ -4,10 +4,10 @@
4
4
  * Owns fail-closed protection, demand-driven pressure retirement, exact-intent admission,
5
5
  * successor recovery, and fenced one-shot deletion before durable slot reuse.
6
6
  */
7
- import type { TelegramUpdateJournalDeadQueueOwnerRecoveryInput, TelegramUpdateJournalDeadQueueOwnerRecoveryResult, TelegramUpdateJournalEntry, TelegramUpdateJournalQueueOwnerIdentity } from "./journal.ts";
8
- import type { TelegramTarget } from "./target.ts";
7
+ import type { TelegramJournalNamespaceInspection, TelegramUpdateJournalDeadQueueOwnerRecoveryInput, TelegramUpdateJournalDeadQueueOwnerRecoveryResult, TelegramUpdateJournalEntry, TelegramUpdateJournalQueueOwnerIdentity } from "./journal.ts";
8
+ import { type TelegramTarget } from "./target.ts";
9
9
  import { type TelegramWorkspaceThreadDeletionTransport } from "./telegram-api.ts";
10
- import type { TelegramTopicTargetStore, TelegramWorkspaceExternalProtectionEvidence, TelegramWorkspaceRetirementIntent, TelegramWorkspaceProtectionState, TelegramWorkspaceThreadBinding } from "./threads.ts";
10
+ import type { TelegramTopicTargetStore, TelegramWorkspaceJournalSource, TelegramWorkspaceExternalProtectionEvidence, TelegramWorkspaceRetirementIntent, TelegramWorkspaceProtectionState, TelegramWorkspaceThreadBinding } from "./threads.ts";
11
11
  import { type TelegramWorkspaceAdmissionLedger, type TelegramWorkspaceAdmissionScope, type TelegramWorkspaceDeletionPermit } from "./workspace-admission.ts";
12
12
  export interface TelegramWorkspaceOperationGate {
13
13
  runExclusive: <T>(operation: () => Promise<T>) => Promise<T>;
@@ -36,18 +36,21 @@ interface TelegramWorkspaceJournalReader {
36
36
  entries: readonly {
37
37
  update: unknown;
38
38
  }[];
39
+ exists?: boolean;
39
40
  };
40
41
  };
41
42
  readForProtection?: () => {
42
43
  entries: readonly {
43
44
  update: unknown;
44
45
  }[];
46
+ exists?: boolean;
45
47
  };
46
48
  }
47
49
  export declare function captureTelegramWorkspaceJournalProtectionSources(input: {
48
50
  binding: TelegramWorkspaceThreadBinding;
49
51
  resolveLeader: () => TelegramWorkspaceJournalReader | undefined;
50
52
  createFollowerResolver: (journalBindingKey: string) => () => TelegramWorkspaceJournalReader | undefined;
53
+ createSessionResolver?: (recipientBindingKey: string, sessionId: string) => () => TelegramWorkspaceJournalReader | undefined;
51
54
  withJournalReference?: <T>(binding: TelegramWorkspaceJournalReader, operation: () => T) => T;
52
55
  discovery?: {
53
56
  paths: readonly string[];
@@ -63,6 +66,7 @@ export type TelegramWorkspaceJournalProtectionSource = {
63
66
  kind: "binding";
64
67
  bindingKey: string;
65
68
  journalBindingKey: string;
69
+ sessionId?: string;
66
70
  } | {
67
71
  kind: "discovered";
68
72
  path: string;
@@ -70,6 +74,8 @@ export type TelegramWorkspaceJournalProtectionSource = {
70
74
  entries: readonly {
71
75
  update: unknown;
72
76
  }[];
77
+ /** Positive filesystem presence is distinct from a complete namespace with a missing historical family. */
78
+ exists?: boolean;
73
79
  } | {
74
80
  kind: "unknown";
75
81
  scope: {
@@ -78,6 +84,7 @@ export type TelegramWorkspaceJournalProtectionSource = {
78
84
  kind: "binding";
79
85
  bindingKey: string;
80
86
  journalBindingKey: string;
87
+ sessionId?: string;
81
88
  } | {
82
89
  kind: "discovered";
83
90
  path: string;
@@ -91,19 +98,24 @@ export declare function resolveTelegramWorkspaceAcceptedWorkProtection(input: {
91
98
  }[];
92
99
  journalSources: readonly TelegramWorkspaceJournalProtectionSource[];
93
100
  sourcesComplete: boolean;
101
+ /** Relocation cannot infer execution ownership from the original Telegram message target. */
102
+ requireBindingProvenance?: boolean;
94
103
  }): TelegramWorkspaceProtectionState;
95
104
  export type TelegramWorkspaceJournalPruneResult = {
96
105
  kind: "committed";
97
106
  binding: TelegramWorkspaceThreadBinding;
98
107
  removedKeys: string[];
108
+ removedSources?: TelegramWorkspaceJournalSource[];
99
109
  } | {
100
110
  kind: "blocked";
101
- reason: "incomplete-evidence" | "writer-not-quiescent" | "state-changed";
111
+ reason: "incomplete-evidence" | "writer-not-quiescent" | "state-changed" | "publication-refused";
102
112
  };
103
113
  export declare function pruneTelegramWorkspaceJournalEvidence(input: {
104
- store: Pick<TelegramTopicTargetStore, "commitWorkspaceJournalEvidence" | "persist">;
114
+ store: Pick<TelegramTopicTargetStore, "commitWorkspaceJournalEvidence" | "persistWorkspaceJournalEvidence">;
105
115
  binding: TelegramWorkspaceThreadBinding;
106
- capture: TelegramWorkspaceJournalProtectionCapture;
116
+ /** Capture fresh source evidence after admission, not a snapshot taken before the lease. */
117
+ capture: () => TelegramWorkspaceJournalProtectionCapture;
118
+ runExclusive: TelegramWorkspaceOperationGate["runExclusive"];
107
119
  getJournalWriterProtection: (journalBindingKey: string) => TelegramWorkspaceProtectionState;
108
120
  getLeaderEpoch: () => number | string | undefined;
109
121
  getProfileKey: () => string;
@@ -113,6 +125,7 @@ export declare function pruneTelegramWorkspaceJournalEvidence(input: {
113
125
  }): Promise<TelegramWorkspaceJournalPruneResult>;
114
126
  export declare function captureTelegramWorkspaceExternalProtection(input: {
115
127
  binding: TelegramWorkspaceThreadBinding;
128
+ requireBindingProvenance?: boolean;
116
129
  getLiveOwnerProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceProtectionState;
117
130
  getLocalAcceptedTargets: (binding: TelegramWorkspaceThreadBinding) => {
118
131
  targets: readonly TelegramTarget[];
@@ -121,7 +134,7 @@ export declare function captureTelegramWorkspaceExternalProtection(input: {
121
134
  captureJournalSources: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceJournalProtectionCapture;
122
135
  getDeliveryAuthorityProtection?: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceProtectionState;
123
136
  }): TelegramWorkspaceExternalProtectionEvidence;
124
- export declare function createTelegramWorkspaceExternalProtectionCapture(deps: {
137
+ interface TelegramWorkspaceProtectionObserverDeps {
125
138
  listFollowers: () => readonly {
126
139
  target?: TelegramTarget;
127
140
  }[];
@@ -132,15 +145,35 @@ export declare function createTelegramWorkspaceExternalProtectionCapture(deps: {
132
145
  }[];
133
146
  resolveLeaderJournal: () => TelegramWorkspaceJournalReader | undefined;
134
147
  createFollowerJournalResolver: (journalBindingKey: string) => () => TelegramWorkspaceJournalReader | undefined;
148
+ createSessionJournalResolver?: (recipientBindingKey: string, sessionId: string) => () => TelegramWorkspaceJournalReader | undefined;
135
149
  discoverFollowerJournals?: () => {
136
150
  paths: readonly string[];
137
151
  complete: boolean;
138
152
  };
153
+ /** Strict read-only namespace evidence; it never substitutes for writer closure. */
154
+ inspectJournalNamespace?: () => TelegramJournalNamespaceInspection;
139
155
  createJournalPathResolver?: (path: string) => () => TelegramWorkspaceJournalReader | undefined;
140
156
  withJournalReference?: <T>(binding: TelegramWorkspaceJournalReader, operation: () => T) => T;
141
157
  getJournalWriterProtection?: (journalBindingKey: string) => TelegramWorkspaceProtectionState;
142
158
  getDeliveryAuthorityProtection?: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceProtectionState;
143
- }): (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
159
+ }
160
+ export interface TelegramWorkspaceProtectionObserver {
161
+ capture: (binding: TelegramWorkspaceThreadBinding, options?: {
162
+ requireBindingProvenance?: boolean;
163
+ }) => TelegramWorkspaceExternalProtectionEvidence;
164
+ captureJournalSources: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceJournalProtectionCapture;
165
+ getJournalWriterProtection: (journalBindingKey: string) => TelegramWorkspaceProtectionState;
166
+ }
167
+ /** Shared read-only observer: protection and metadata pruning use the same scoped evidence path. */
168
+ export declare function createTelegramWorkspaceProtectionObserver(deps: TelegramWorkspaceProtectionObserverDeps): TelegramWorkspaceProtectionObserver;
169
+ /** Callable protection view retained for callers that need no metadata observation ports. */
170
+ export declare function createTelegramWorkspaceExternalProtectionCapture(deps: TelegramWorkspaceProtectionObserverDeps): TelegramWorkspaceProtectionObserver["capture"];
171
+ export declare function createTelegramWorkspaceJournalEvidencePruner(deps: TelegramWorkspaceOperationGate & {
172
+ store: TelegramTopicTargetStore;
173
+ getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
174
+ getLeaderEpoch: () => number | string | undefined;
175
+ protection: Pick<TelegramWorkspaceProtectionObserver, "captureJournalSources" | "getJournalWriterProtection">;
176
+ }): (binding: TelegramWorkspaceThreadBinding, isCurrent: () => boolean) => Promise<TelegramWorkspaceJournalPruneResult>;
144
177
  export declare function isCurrentTelegramWorkspaceBinding(store: Pick<TelegramTopicTargetStore, "listWorkspaceBindings">, expected: TelegramWorkspaceThreadBinding): boolean;
145
178
  export interface TelegramWorkspaceDeadQueueJournalBinding {
146
179
  recoveryKey?: string;
@@ -175,6 +208,7 @@ export declare function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
175
208
  }[];
176
209
  resolveLeaderJournal: () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
177
210
  createFollowerJournalResolver: (journalBindingKey: string) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
211
+ createSessionJournalResolver?: (recipientBindingKey: string, sessionId: string) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
178
212
  discoverFollowerJournals?: () => {
179
213
  paths: readonly string[];
180
214
  complete: boolean;
@@ -218,7 +252,7 @@ export type TelegramWorkspaceRetirementExecution = {
218
252
  };
219
253
  export type TelegramWorkspaceRetirementAbsence = "absent" | "present" | "unknown";
220
254
  export declare function executeTelegramWorkspaceRetirement(input: {
221
- store: Pick<TelegramTopicTargetStore, "captureWorkspaceSlotOccupancy" | "listWorkspaceBindings" | "listWorkspaceRetirementIntents" | "commitWorkspaceRetirement" | "removeWorkspaceRetirementIntent" | "persist">;
255
+ store: Pick<TelegramTopicTargetStore, "captureWorkspaceSlotOccupancy" | "listWorkspaceBindings" | "listWorkspaceRetirementIntents" | "commitWorkspaceRetirement" | "removeWorkspaceRetirementIntent" | "markStaleByTarget" | "persist">;
222
256
  admission: Pick<TelegramWorkspaceAdmissionLedger, "getOwner" | "read" | "acquireRetirementFence" | "adoptRetirementFence" | "issueDeletionPermit" | "confirmRetirementAbsence" | "confirmRetirementRejection" | "completeRejectedRetirementFence" | "releaseUnissuedRetirementFence" | "completeRetirementFence">;
223
257
  intent: TelegramWorkspaceRetirementIntent;
224
258
  getExternalProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
@@ -263,7 +297,7 @@ export type TelegramWorkspaceRetirementLifecycleResult = TelegramWorkspaceRetire
263
297
  reason: string;
264
298
  };
265
299
  export declare function runTelegramWorkspaceRetirementLifecycle(input: {
266
- store: TelegramWorkspaceRetirementPreparationDeps["store"] & Pick<TelegramTopicTargetStore, "replaceWorkspaceRetirementIntent" | "commitWorkspaceRetirement">;
300
+ store: TelegramWorkspaceRetirementPreparationDeps["store"] & Pick<TelegramTopicTargetStore, "replaceWorkspaceRetirementIntent" | "commitWorkspaceRetirement" | "markStaleByTarget">;
267
301
  getExternalProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
268
302
  getLeaderEpoch: () => number | string | undefined;
269
303
  getProfileKey: () => string;
@@ -278,6 +312,7 @@ export type TelegramWorkspaceCapacityRunner = <T>(operation: () => Promise<T>) =
278
312
  export interface TelegramWorkspaceSlotRotationPorts extends TelegramWorkspaceOperationGate {
279
313
  getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
280
314
  deleteThread: TelegramWorkspaceThreadDeletionTransport;
315
+ pruneJournalEvidence?: (binding: TelegramWorkspaceThreadBinding, isCurrent: () => boolean) => Promise<TelegramWorkspaceJournalPruneResult>;
281
316
  reclaimDeadOwnerQueuedWork?: (binding: TelegramWorkspaceThreadBinding, isCurrent: () => boolean) => Promise<TelegramWorkspaceDeadQueueReclamation>;
282
317
  }
283
318
  /** Retry allocation once, only after the failed operation released all ordinary leases. */
@@ -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;
@@ -605,6 +680,12 @@ export async function executeTelegramWorkspaceRetirement(input) {
605
680
  }
606
681
  return { kind: "retained", reason: "stale-intent" };
607
682
  }
683
+ if (fence?.phase === "commit-ready") {
684
+ // Exact confirmed absence retires any same-target routing record that
685
+ // survived the deletion attempt. The fence prevents replacement target
686
+ // publication while this stale local projection is removed.
687
+ input.store.markStaleByTarget(binding.target, "deleted");
688
+ }
608
689
  const eligible = () => input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection, { expectedRetirement: input.intent }).bindings.find((candidate) => candidate.bindingKey === input.intent.binding.bindingKey)?.protection === "eligible";
609
690
  if (!eligible())
610
691
  return { kind: "retained", reason: "protection-changed" };
@@ -669,6 +750,9 @@ export async function executeTelegramWorkspaceRetirement(input) {
669
750
  }
670
751
  if (!isCurrent())
671
752
  return { kind: "retained", reason: "authority-changed" };
753
+ if (fence.phase === "commit-ready") {
754
+ input.store.markStaleByTarget(binding.target, "deleted");
755
+ }
672
756
  if (!eligible())
673
757
  return { kind: "retained", reason: "protection-changed" };
674
758
  if (!await input.store.commitWorkspaceRetirement(input.intent, isCurrent)) {
@@ -799,19 +883,46 @@ export async function runTelegramWorkspaceRetirementLifecycle(input) {
799
883
  });
800
884
  }
801
885
  if (intent) {
802
- const adoption = await adoptTelegramWorkspaceRetirementIntent({
803
- store: input.store,
804
- intent,
805
- getExternalProtection: input.getExternalProtection,
806
- getLeaderEpoch: input.getLeaderEpoch,
807
- getProfileKey: input.getProfileKey,
808
- isCurrent: input.isCurrent,
809
- runExclusive: input.runExclusive,
810
- });
811
- if (adoption.kind !== "adopted") {
812
- return { kind: "blocked", stage: "adoption", reason: adoption.reason };
886
+ if (retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
887
+ retainedFence.phase === "commit-ready" &&
888
+ matchesTelegramWorkspaceRetirementFence(retainedFence, intent)) {
889
+ const resumed = await input.runExclusive(async () => {
890
+ const epoch = input.getLeaderEpoch();
891
+ const profileKey = input.getProfileKey();
892
+ const isCurrent = () => epoch !== undefined && input.getLeaderEpoch() === epoch &&
893
+ input.getProfileKey() === profileKey && input.isCurrent?.() !== false;
894
+ if (!isCurrent() || intent.profileKey !== profileKey)
895
+ return undefined;
896
+ const intents = input.store.listWorkspaceRetirementIntents();
897
+ const binding = input.store.listWorkspaceBindings().find(candidate => candidate.bindingKey === intent.binding.bindingKey);
898
+ if (intents.length !== 1 || !isDeepStrictEqual(intents[0], intent) ||
899
+ !binding || !isDeepStrictEqual(binding, intent.binding))
900
+ return undefined;
901
+ if (intent.leaderEpoch === epoch)
902
+ return intent;
903
+ const replacement = { ...intent, leaderEpoch: epoch };
904
+ return await input.store.replaceWorkspaceRetirementIntent(intent, replacement, isCurrent)
905
+ ? replacement : undefined;
906
+ });
907
+ if (!resumed)
908
+ return { kind: "blocked", stage: "adoption", reason: "commit-rejected" };
909
+ intent = resumed;
910
+ }
911
+ else {
912
+ const adoption = await adoptTelegramWorkspaceRetirementIntent({
913
+ store: input.store,
914
+ intent,
915
+ getExternalProtection: input.getExternalProtection,
916
+ getLeaderEpoch: input.getLeaderEpoch,
917
+ getProfileKey: input.getProfileKey,
918
+ isCurrent: input.isCurrent,
919
+ runExclusive: input.runExclusive,
920
+ });
921
+ if (adoption.kind !== "adopted") {
922
+ return { kind: "blocked", stage: "adoption", reason: adoption.reason };
923
+ }
924
+ intent = adoption.intent;
813
925
  }
814
- intent = adoption.intent;
815
926
  }
816
927
  else {
817
928
  const preparation = await prepareTelegramWorkspaceRetirement({
@@ -921,21 +1032,42 @@ export function createTelegramWorkspaceSlotRotation(input) {
921
1032
  catch (error) {
922
1033
  if (!(error instanceof TelegramWorkspaceSlotUnavailableError))
923
1034
  throw error;
924
- if (input.reclaimDeadOwnerQueuedWork) {
1035
+ if (input.pruneJournalEvidence || input.reclaimDeadOwnerQueuedWork) {
925
1036
  const candidates = await input.runExclusive(async () => {
926
1037
  await input.store.load();
927
1038
  if (!isCurrent())
928
1039
  throw new Error("Telegram Workspace reclamation lost leader authority.");
929
1040
  const snapshot = input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection);
930
1041
  const allocation = planTelegramWorkspaceSlotAllocation({ ...snapshot, nowMs: Date.now() });
931
- 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)
932
1045
  return [];
933
- 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" &&
934
1048
  Number.isFinite(binding.inactiveSinceMs) &&
935
1049
  binding.inactiveSinceMs >= 0).sort((left, right) => left.inactiveSinceMs - right.inactiveSinceMs ||
936
1050
  (left.slot ?? "").localeCompare(right.slot ?? ""));
937
1051
  });
938
- 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;
939
1071
  const evidence = input.getExternalProtection(binding);
940
1072
  if (evidence.liveOwner !== "clear" ||
941
1073
  evidence.acceptedWork !== "protected" ||
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.51.5",
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.