@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
@@ -187,7 +187,8 @@ interface TelegramWorkspaceRetirementFenceBase {
187
187
  retirementIntentId: string;
188
188
  profileKey: string;
189
189
  bindingKey: string;
190
- slot: string;
190
+ /** Null is admitted only for explicit manual cleanup of a slotless binding. */
191
+ slot: string | null;
191
192
  target: TelegramTarget & { threadId: number };
192
193
  leaderEpoch: number | string;
193
194
  retirementRequestedAtMs: number;
@@ -228,7 +229,7 @@ export interface TelegramWorkspaceDeletionPermit {
228
229
  readonly retirementIntentId: string;
229
230
  readonly profileKey: string;
230
231
  readonly bindingKey: string;
231
- readonly slot: string;
232
+ readonly slot: string | null;
232
233
  readonly target: TelegramTarget & { threadId: number };
233
234
  readonly leaderEpoch: number | string;
234
235
  readonly issuedAtMs: number;
@@ -376,7 +377,7 @@ export interface TelegramWorkspaceAdmissionLedger {
376
377
  operationId: string;
377
378
  cleanupWorkSetId: string;
378
379
  bindingKey: string;
379
- slot: string;
380
+ slot: string | null;
380
381
  target: TelegramTarget & { threadId: number };
381
382
  leaderEpoch: number | string;
382
383
  cleanupRequestedAtMs: number;
@@ -538,8 +539,8 @@ function normalizeFence(
538
539
  !isBoundedText(value.retirementIntentId) ||
539
540
  value.profileKey !== profileKey ||
540
541
  !isBoundedText(value.bindingKey) ||
541
- typeof value.slot !== "string" ||
542
- !/^[A-Z]$/u.test(value.slot) ||
542
+ !((typeof value.slot === "string" && /^[A-Z]$/u.test(value.slot)) ||
543
+ (value.destructiveKind === "manual-thread-cleanup" && value.slot === null)) ||
543
544
  !target ||
544
545
  !isLeaderEpoch(value.leaderEpoch) ||
545
546
  !isSafeTimestamp(value.retirementRequestedAtMs) ||
@@ -1869,7 +1870,7 @@ export function createTelegramWorkspaceAdmissionLedger(
1869
1870
  operationId: string;
1870
1871
  retirementIntentId: string;
1871
1872
  bindingKey: string;
1872
- slot: string;
1873
+ slot: string | null;
1873
1874
  target: TelegramTarget & { threadId: number };
1874
1875
  leaderEpoch: number | string;
1875
1876
  retirementRequestedAtMs: number;
@@ -2307,7 +2308,7 @@ export function createTelegramWorkspaceAdmissionLedger(
2307
2308
  }),
2308
2309
  listReservedSlots: () => {
2309
2310
  const fence = read().fence;
2310
- return fence && isTelegramWorkspaceRetirementFence(fence)
2311
+ return fence && isTelegramWorkspaceRetirementFence(fence) && fence.slot !== null
2311
2312
  ? [fence.slot]
2312
2313
  : [];
2313
2314
  },
@@ -1764,6 +1764,8 @@ export async function runTelegramWorkspaceRetirementLifecycle(input: {
1764
1764
  owner,
1765
1765
  leaderEpoch: epoch,
1766
1766
  });
1767
+ if (fence.slot === null)
1768
+ return { kind: "retained", reason: "stale-intent" };
1767
1769
  input.admission.completeRetirementFence(fence);
1768
1770
  return {
1769
1771
  kind: "retired",
@@ -1892,6 +1894,7 @@ export function createTelegramWorkspaceSlotRotation(
1892
1894
  binding: TelegramWorkspaceThreadBinding,
1893
1895
  ) => TelegramWorkspaceExternalProtectionEvidence;
1894
1896
  recordEvent: (message: string, details: Record<string, unknown>) => void;
1897
+ onSlotReused?: (bindingKey: string, isCurrent: () => boolean) => Promise<void>;
1895
1898
  },
1896
1899
  ): TelegramWorkspaceCapacityRunner {
1897
1900
  const requests = createTelegramWorkspaceOperationGate();
@@ -1906,7 +1909,7 @@ export function createTelegramWorkspaceSlotRotation(
1906
1909
  return requests.runExclusive(async () => {
1907
1910
  if (!isCurrent())
1908
1911
  throw new Error("Telegram Workspace allocation lost leader authority.");
1909
- const rotate = async () => {
1912
+ const rotate = async (options?: { reuseWhenProtected?: boolean }) => {
1910
1913
  const result = await input.runExclusive(async () => {
1911
1914
  if (!isCurrent())
1912
1915
  throw new Error(
@@ -1959,6 +1962,57 @@ export function createTelegramWorkspaceSlotRotation(
1959
1962
  },
1960
1963
  });
1961
1964
  });
1965
+ if (
1966
+ options?.reuseWhenProtected &&
1967
+ result.kind === "blocked" &&
1968
+ result.stage === "preparation" &&
1969
+ result.reason === "protected-capacity"
1970
+ ) {
1971
+ // Every letter is protected, so deleting a Thread is off the table: reassign the
1972
+ // least recently used letter as metadata and keep its Thread, journals and work.
1973
+ const reused = await input.runExclusive(async () => {
1974
+ if (!isCurrent())
1975
+ throw new Error(
1976
+ "Telegram Workspace slot reuse lost leader authority.",
1977
+ );
1978
+ await input.store.load();
1979
+ if (!isCurrent())
1980
+ throw new Error(
1981
+ "Telegram Workspace slot reuse lost leader authority.",
1982
+ );
1983
+ return input.store.reuseLeastRecentlyUsedWorkspaceSlot(isCurrent);
1984
+ });
1985
+ if (!isCurrent())
1986
+ throw new Error("Telegram Workspace rotation lost leader authority.");
1987
+ if (reused) {
1988
+ if (reused.kind === "reused") {
1989
+ // The store commit is final. Await display repair before the next allocation,
1990
+ // but never roll back or repeat a published reuse after a transport failure.
1991
+ try {
1992
+ await input.onSlotReused?.(reused.bindingKey, isCurrent);
1993
+ } catch (error) {
1994
+ try {
1995
+ input.recordEvent("Telegram Workspace slot display update failed.", {
1996
+ slot: reused.slot,
1997
+ error: error instanceof Error ? error.message : String(error),
1998
+ });
1999
+ } catch {
2000
+ /* A diagnostic failure grants no rollback or another display attempt. */
2001
+ }
2002
+ }
2003
+ if (!isCurrent())
2004
+ throw new Error("Telegram Workspace rotation lost leader authority.");
2005
+ try {
2006
+ input.recordEvent("Telegram Workspace slot reused.", {
2007
+ slot: reused.slot,
2008
+ });
2009
+ } catch {
2010
+ /* Diagnostics cannot turn a published reassignment into another attempt. */
2011
+ }
2012
+ }
2013
+ return { kind: "reused" as const };
2014
+ }
2015
+ }
1962
2016
  if (
1963
2017
  result.kind !== "retired" &&
1964
2018
  result.kind !== "not-needed" &&
@@ -2104,7 +2158,7 @@ export function createTelegramWorkspaceSlotRotation(
2104
2158
  }
2105
2159
  }
2106
2160
  }
2107
- await rotate();
2161
+ await rotate({ reuseWhenProtected: true });
2108
2162
  return operation();
2109
2163
  }
2110
2164
  });
@@ -27,6 +27,50 @@ export type TelegramWorkspaceSlotAllocation =
27
27
  | { kind: "reclaim"; candidate: TelegramWorkspaceSlotOccupancy }
28
28
  | { kind: "blocked"; reason: "invalid-state" | "protected-capacity" };
29
29
 
30
+ /** A binding whose letter may be reassigned without touching its Thread, journals or accepted work. */
31
+ export interface TelegramWorkspaceReusableSlot {
32
+ bindingKey: string;
33
+ /** Uppercase display letter currently held by the binding. */
34
+ slot: string;
35
+ /** A live owner counts as most recently used, whatever its stored clocks say. */
36
+ live: boolean;
37
+ /** Last known use: inactivity start for detached bindings, otherwise the last binding update. */
38
+ lastUsedMs: number;
39
+ }
40
+
41
+ /**
42
+ * Metadata-only fallback for a full A-Z pool: pick the least recently used letter,
43
+ * preferring detached bindings over live ones. Unlike pressure retirement this
44
+ * ignores accepted-work protection because reuse deletes nothing.
45
+ * Returns undefined for an empty or malformed candidate set.
46
+ */
47
+ export function selectTelegramWorkspaceReusableSlot(
48
+ candidates: readonly TelegramWorkspaceReusableSlot[],
49
+ ): TelegramWorkspaceReusableSlot | undefined {
50
+ const slots = new Set<string>();
51
+ const keys = new Set<string>();
52
+ for (const candidate of candidates) {
53
+ if (
54
+ !/^[A-Z]$/u.test(candidate.slot) ||
55
+ !candidate.bindingKey ||
56
+ !Number.isSafeInteger(candidate.lastUsedMs) ||
57
+ candidate.lastUsedMs < 0 ||
58
+ slots.has(candidate.slot) ||
59
+ keys.has(candidate.bindingKey)
60
+ )
61
+ return undefined;
62
+ slots.add(candidate.slot);
63
+ keys.add(candidate.bindingKey);
64
+ }
65
+ const [selected] = [...candidates].sort(
66
+ (left, right) =>
67
+ Number(left.live) - Number(right.live) ||
68
+ left.lastUsedMs - right.lastUsedMs ||
69
+ left.slot.localeCompare(right.slot),
70
+ );
71
+ return selected ? { ...selected } : undefined;
72
+ }
73
+
30
74
  function isSlot(slot: string): boolean {
31
75
  return /^[a-z]$/u.test(slot);
32
76
  }
@@ -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",
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Deterministic macOS CI test shards
3
+ * Zones: CI, test execution
4
+ * Keeps real-process tests isolated and partitions the remaining files without changing test deadlines.
5
+ */
6
+ import { spawnSync } from "node:child_process";
7
+ import { readdirSync } from "node:fs";
8
+ import { delimiter, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const [shard, phase, mode, ...extra] = process.argv.slice(2);
12
+ if (!["0", "1"].includes(shard) || !["process", "remaining"].includes(phase) ||
13
+ (mode !== undefined && mode !== "--list") || extra.length) {
14
+ throw new Error("Usage: run-macos-tests.mjs <0|1> <process|remaining> [--list]");
15
+ }
16
+ const root = fileURLToPath(new URL("..", import.meta.url));
17
+ const entries = readdirSync(join(root, "tests"), { withFileTypes: true })
18
+ .filter((entry) => entry.name.endsWith(".test.ts"));
19
+ if (entries.some((entry) => !entry.isFile()))
20
+ throw new Error("The CI test inventory must contain only regular test files.");
21
+ const names = entries.map((entry) => entry.name).sort();
22
+ const processTest = "process-shutdown.test.ts";
23
+ if (!names.includes(processTest)) throw new Error("The real-process test suite is missing.");
24
+ const files = (phase === "process"
25
+ ? (shard === "0" ? [processTest] : [])
26
+ : names.filter((name) => name !== processTest).filter((_, index) => index % 2 === Number(shard)))
27
+ .map((name) => `tests/${name}`);
28
+ if (mode === "--list") {
29
+ console.log(JSON.stringify(files));
30
+ } else if (files.length) {
31
+ console.log(`macOS shard ${Number(shard) + 1}/2 ${phase}: ${files.length} test file(s)`);
32
+ const result = spawnSync(process.execPath, [
33
+ "--experimental-strip-types", "--import", "./tests/fixtures/canonical-tmpdir.ts",
34
+ "--test", "--test-concurrency=1", `--test-reporter=${phase === "process" ? "spec" : "dot"}`,
35
+ ...files,
36
+ ], {
37
+ cwd: root,
38
+ stdio: "inherit",
39
+ env: { ...process.env, PATH: [join(root, "node_modules", ".bin"), process.env.PATH].filter(Boolean).join(delimiter) },
40
+ });
41
+ if (result.error) throw result.error;
42
+ process.exitCode = result.status ?? 1;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.36.1",
3
+ "version": "0.36.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -45,12 +45,12 @@
45
45
  ],
46
46
  "dependencies": {
47
47
  "@llblab/pi-actors": "0.55.0",
48
- "@llblab/pi-claude-usage": "0.2.2",
48
+ "@llblab/pi-claude-usage": "0.2.3",
49
49
  "@llblab/pi-clean-room": "0.3.0",
50
- "@llblab/pi-codex-usage": "0.13.1",
50
+ "@llblab/pi-codex-usage": "0.13.2",
51
51
  "@llblab/pi-grow-loop": "0.9.0",
52
52
  "@llblab/pi-state-flow": "0.26.5",
53
- "@llblab/pi-telegram": "0.57.3",
53
+ "@llblab/pi-telegram": "0.57.11",
54
54
  "@llblab/skills": "1.15.0"
55
55
  },
56
56
  "bundledDependencies": [