@llblab/pi-kit 0.36.1 → 0.36.3

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 (77) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +4 -0
  4. package/node_modules/@llblab/pi-claude-usage/README.md +2 -2
  5. package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +2 -3
  6. package/node_modules/@llblab/pi-claude-usage/package.json +1 -1
  7. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
  8. package/node_modules/@llblab/pi-codex-usage/README.md +1 -1
  9. package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +2 -3
  10. package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
  11. package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
  12. package/node_modules/@llblab/pi-telegram/BACKLOG.md +43 -5
  13. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +45 -0
  14. package/node_modules/@llblab/pi-telegram/README.md +4 -2
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +17 -1
  16. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +23 -11
  17. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +24 -6
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +2 -1
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +7 -0
  20. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +7 -4
  21. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +22 -4
  22. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +52 -10
  23. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +1 -0
  24. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +9 -0
  25. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
  26. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +1 -1
  27. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +3 -2
  28. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +4 -4
  29. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -2
  30. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +6 -3
  31. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +50 -9
  32. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +3 -2
  33. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +120 -14
  34. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +13 -0
  35. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +12 -0
  36. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +39 -3
  37. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +144 -16
  38. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -1
  39. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +15 -8
  40. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +14 -1
  41. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +122 -8
  42. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +9 -6
  43. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +4 -3
  44. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +3 -3
  45. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -0
  46. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +52 -2
  47. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +17 -0
  48. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +25 -0
  49. package/node_modules/@llblab/pi-telegram/dist/package.json +2 -1
  50. package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -10
  51. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +23 -3
  52. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -0
  53. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +1 -1
  54. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +24 -1
  55. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +30 -13
  56. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +25 -8
  57. package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -0
  58. package/node_modules/@llblab/pi-telegram/lib/commands.ts +34 -8
  59. package/node_modules/@llblab/pi-telegram/lib/extension.ts +54 -11
  60. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +12 -0
  61. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +2 -0
  62. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +4 -3
  63. package/node_modules/@llblab/pi-telegram/lib/menu.ts +4 -4
  64. package/node_modules/@llblab/pi-telegram/lib/pi.ts +2 -0
  65. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +79 -16
  66. package/node_modules/@llblab/pi-telegram/lib/routing.ts +142 -26
  67. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +18 -0
  68. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +173 -17
  69. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +14 -8
  70. package/node_modules/@llblab/pi-telegram/lib/threads.ts +166 -10
  71. package/node_modules/@llblab/pi-telegram/lib/updates.ts +9 -6
  72. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +8 -7
  73. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +56 -2
  74. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +44 -0
  75. package/node_modules/@llblab/pi-telegram/package.json +2 -1
  76. package/node_modules/@llblab/pi-telegram/scripts/run-macos-tests.mjs +43 -0
  77. package/package.json +4 -4
@@ -6359,12 +6359,13 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
6359
6359
  const typedUpdate = update;
6360
6360
  const prepared = deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate;
6361
6361
  // Journal/source evidence remains the original; only the warm released handler projection changes destination.
6362
- const routed = target ? { ...prepared,
6363
- ...(prepared.message ? { message: prepared.message.message_thread_id === target.threadId ? { ...prepared.message }
6364
- : withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id: target.threadId, reply_to_message: undefined }) } : {}),
6365
- ...(prepared.edited_message ? { edited_message: prepared.edited_message.message_thread_id === target.threadId ? { ...prepared.edited_message }
6366
- : withoutTelegramTurnReplyAnchor({ ...prepared.edited_message, message_thread_id: target.threadId, reply_to_message: undefined }) } : {}),
6367
- } : prepared;
6362
+ const projectTo = (destination) => ({ ...prepared,
6363
+ ...(prepared.message ? { message: prepared.message.message_thread_id === destination.threadId ? { ...prepared.message }
6364
+ : withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id: destination.threadId, reply_to_message: undefined }) } : {}),
6365
+ ...(prepared.edited_message ? { edited_message: prepared.edited_message.message_thread_id === destination.threadId ? { ...prepared.edited_message }
6366
+ : withoutTelegramTurnReplyAnchor({ ...prepared.edited_message, message_thread_id: destination.threadId, reply_to_message: undefined }) } : {}),
6367
+ });
6368
+ const routed = target ? projectTo(target) : prepared;
6368
6369
  try {
6369
6370
  return await executeUpdate(routed, ctx, signal);
6370
6371
  }
@@ -6372,6 +6373,8 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
6372
6373
  if (await deps.settleTerminalExecutionFailure?.(error)) {
6373
6374
  return { kind: "complete" };
6374
6375
  }
6376
+ // A refused reply proves only that send was not issued, not that the handler made no prior effects.
6377
+ // Leave failure settlement to the worker's retry policy; never re-enter the handler to recover delivery here.
6375
6378
  throw error;
6376
6379
  }
6377
6380
  },
@@ -61,7 +61,8 @@ interface TelegramWorkspaceRetirementFenceBase {
61
61
  retirementIntentId: string;
62
62
  profileKey: string;
63
63
  bindingKey: string;
64
- slot: string;
64
+ /** Null is admitted only for explicit manual cleanup of a slotless binding. */
65
+ slot: string | null;
65
66
  target: TelegramTarget & {
66
67
  threadId: number;
67
68
  };
@@ -93,7 +94,7 @@ export interface TelegramWorkspaceDeletionPermit {
93
94
  readonly retirementIntentId: string;
94
95
  readonly profileKey: string;
95
96
  readonly bindingKey: string;
96
- readonly slot: string;
97
+ readonly slot: string | null;
97
98
  readonly target: TelegramTarget & {
98
99
  threadId: number;
99
100
  };
@@ -205,7 +206,7 @@ export interface TelegramWorkspaceAdmissionLedger {
205
206
  operationId: string;
206
207
  cleanupWorkSetId: string;
207
208
  bindingKey: string;
208
- slot: string;
209
+ slot: string | null;
209
210
  target: TelegramTarget & {
210
211
  threadId: number;
211
212
  };
@@ -194,8 +194,8 @@ function normalizeFence(value, profileKey) {
194
194
  !isBoundedText(value.retirementIntentId) ||
195
195
  value.profileKey !== profileKey ||
196
196
  !isBoundedText(value.bindingKey) ||
197
- typeof value.slot !== "string" ||
198
- !/^[A-Z]$/u.test(value.slot) ||
197
+ !((typeof value.slot === "string" && /^[A-Z]$/u.test(value.slot)) ||
198
+ (value.destructiveKind === "manual-thread-cleanup" && value.slot === null)) ||
199
199
  !target ||
200
200
  !isLeaderEpoch(value.leaderEpoch) ||
201
201
  !isSafeTimestamp(value.retirementRequestedAtMs) ||
@@ -1442,7 +1442,7 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
1442
1442
  }),
1443
1443
  listReservedSlots: () => {
1444
1444
  const fence = read().fence;
1445
- return fence && isTelegramWorkspaceRetirementFence(fence)
1445
+ return fence && isTelegramWorkspaceRetirementFence(fence) && fence.slot !== null
1446
1446
  ? [fence.slot]
1447
1447
  : [];
1448
1448
  },
@@ -325,5 +325,6 @@ export declare function createTelegramWorkspaceSlotRotation(input: TelegramWorks
325
325
  getLeaderEpoch: () => number | string | undefined;
326
326
  getExternalProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
327
327
  recordEvent: (message: string, details: Record<string, unknown>) => void;
328
+ onSlotReused?: (bindingKey: string, isCurrent: () => boolean) => Promise<void>;
328
329
  }): TelegramWorkspaceCapacityRunner;
329
330
  export {};
@@ -1053,6 +1053,8 @@ export async function runTelegramWorkspaceRetirementLifecycle(input) {
1053
1053
  owner,
1054
1054
  leaderEpoch: epoch,
1055
1055
  });
1056
+ if (fence.slot === null)
1057
+ return { kind: "retained", reason: "stale-intent" };
1056
1058
  input.admission.completeRetirementFence(fence);
1057
1059
  return {
1058
1060
  kind: "retired",
@@ -1162,7 +1164,7 @@ export function createTelegramWorkspaceSlotRotation(input) {
1162
1164
  return requests.runExclusive(async () => {
1163
1165
  if (!isCurrent())
1164
1166
  throw new Error("Telegram Workspace allocation lost leader authority.");
1165
- const rotate = async () => {
1167
+ const rotate = async (options) => {
1166
1168
  const result = await input.runExclusive(async () => {
1167
1169
  if (!isCurrent())
1168
1170
  throw new Error("Telegram Workspace rotation lost leader authority.");
@@ -1207,6 +1209,54 @@ export function createTelegramWorkspaceSlotRotation(input) {
1207
1209
  },
1208
1210
  });
1209
1211
  });
1212
+ if (options?.reuseWhenProtected &&
1213
+ result.kind === "blocked" &&
1214
+ result.stage === "preparation" &&
1215
+ result.reason === "protected-capacity") {
1216
+ // Every letter is protected, so deleting a Thread is off the table: reassign the
1217
+ // least recently used letter as metadata and keep its Thread, journals and work.
1218
+ const reused = await input.runExclusive(async () => {
1219
+ if (!isCurrent())
1220
+ throw new Error("Telegram Workspace slot reuse lost leader authority.");
1221
+ await input.store.load();
1222
+ if (!isCurrent())
1223
+ throw new Error("Telegram Workspace slot reuse lost leader authority.");
1224
+ return input.store.reuseLeastRecentlyUsedWorkspaceSlot(isCurrent);
1225
+ });
1226
+ if (!isCurrent())
1227
+ throw new Error("Telegram Workspace rotation lost leader authority.");
1228
+ if (reused) {
1229
+ if (reused.kind === "reused") {
1230
+ // The store commit is final. Await display repair before the next allocation,
1231
+ // but never roll back or repeat a published reuse after a transport failure.
1232
+ try {
1233
+ await input.onSlotReused?.(reused.bindingKey, isCurrent);
1234
+ }
1235
+ catch (error) {
1236
+ try {
1237
+ input.recordEvent("Telegram Workspace slot display update failed.", {
1238
+ slot: reused.slot,
1239
+ error: error instanceof Error ? error.message : String(error),
1240
+ });
1241
+ }
1242
+ catch {
1243
+ /* A diagnostic failure grants no rollback or another display attempt. */
1244
+ }
1245
+ }
1246
+ if (!isCurrent())
1247
+ throw new Error("Telegram Workspace rotation lost leader authority.");
1248
+ try {
1249
+ input.recordEvent("Telegram Workspace slot reused.", {
1250
+ slot: reused.slot,
1251
+ });
1252
+ }
1253
+ catch {
1254
+ /* Diagnostics cannot turn a published reassignment into another attempt. */
1255
+ }
1256
+ }
1257
+ return { kind: "reused" };
1258
+ }
1259
+ }
1210
1260
  if (result.kind !== "retired" &&
1211
1261
  result.kind !== "not-needed" &&
1212
1262
  result.kind !== "cancelled") {
@@ -1320,7 +1370,7 @@ export function createTelegramWorkspaceSlotRotation(input) {
1320
1370
  }
1321
1371
  }
1322
1372
  }
1323
- await rotate();
1373
+ await rotate({ reuseWhenProtected: true });
1324
1374
  return operation();
1325
1375
  }
1326
1376
  });
@@ -25,6 +25,23 @@ export type TelegramWorkspaceSlotAllocation = {
25
25
  kind: "blocked";
26
26
  reason: "invalid-state" | "protected-capacity";
27
27
  };
28
+ /** A binding whose letter may be reassigned without touching its Thread, journals or accepted work. */
29
+ export interface TelegramWorkspaceReusableSlot {
30
+ bindingKey: string;
31
+ /** Uppercase display letter currently held by the binding. */
32
+ slot: string;
33
+ /** A live owner counts as most recently used, whatever its stored clocks say. */
34
+ live: boolean;
35
+ /** Last known use: inactivity start for detached bindings, otherwise the last binding update. */
36
+ lastUsedMs: number;
37
+ }
38
+ /**
39
+ * Metadata-only fallback for a full A-Z pool: pick the least recently used letter,
40
+ * preferring detached bindings over live ones. Unlike pressure retirement this
41
+ * ignores accepted-work protection because reuse deletes nothing.
42
+ * Returns undefined for an empty or malformed candidate set.
43
+ */
44
+ export declare function selectTelegramWorkspaceReusableSlot(candidates: readonly TelegramWorkspaceReusableSlot[]): TelegramWorkspaceReusableSlot | undefined;
28
45
  /** Caller must recheck exact ownership and protected work before retirement. */
29
46
  export declare function planTelegramWorkspaceSlotAllocation(input: {
30
47
  bindings: readonly TelegramWorkspaceSlotOccupancy[];
@@ -12,6 +12,31 @@ export class TelegramWorkspaceSlotUnavailableError extends Error {
12
12
  this.name = "TelegramWorkspaceSlotUnavailableError";
13
13
  }
14
14
  }
15
+ /**
16
+ * Metadata-only fallback for a full A-Z pool: pick the least recently used letter,
17
+ * preferring detached bindings over live ones. Unlike pressure retirement this
18
+ * ignores accepted-work protection because reuse deletes nothing.
19
+ * Returns undefined for an empty or malformed candidate set.
20
+ */
21
+ export function selectTelegramWorkspaceReusableSlot(candidates) {
22
+ const slots = new Set();
23
+ const keys = new Set();
24
+ for (const candidate of candidates) {
25
+ if (!/^[A-Z]$/u.test(candidate.slot) ||
26
+ !candidate.bindingKey ||
27
+ !Number.isSafeInteger(candidate.lastUsedMs) ||
28
+ candidate.lastUsedMs < 0 ||
29
+ slots.has(candidate.slot) ||
30
+ keys.has(candidate.bindingKey))
31
+ return undefined;
32
+ slots.add(candidate.slot);
33
+ keys.add(candidate.bindingKey);
34
+ }
35
+ const [selected] = [...candidates].sort((left, right) => Number(left.live) - Number(right.live) ||
36
+ left.lastUsedMs - right.lastUsedMs ||
37
+ left.slot.localeCompare(right.slot));
38
+ return selected ? { ...selected } : undefined;
39
+ }
15
40
  function isSlot(slot) {
16
41
  return /^[a-z]$/u.test(slot);
17
42
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.57.3",
3
+ "version": "0.57.11",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "scripts": {
30
30
  "test": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-concurrency=4 --test-reporter=dot tests/*.test.ts",
31
+ "test:low-concurrency": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-concurrency=2 --test-reporter=dot tests/*.test.ts",
31
32
  "test:verbose": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-reporter=spec tests/*.test.ts",
32
33
  "typecheck": "tsc --noEmit",
33
34
  "build": "node scripts/build-dist.mjs",