@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.
- package/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +2 -2
- package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +2 -3
- package/node_modules/@llblab/pi-claude-usage/package.json +1 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +1 -1
- package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +2 -3
- package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +43 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +45 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +17 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +23 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +24 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +7 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +22 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +52 -10
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +6 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +50 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +120 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +39 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +144 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +15 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +14 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +122 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +9 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +52 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +17 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +25 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +2 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -10
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +23 -3
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +24 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +30 -13
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +25 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +34 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +54 -11
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +12 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +4 -3
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +79 -16
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +142 -26
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +18 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +173 -17
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +14 -8
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +166 -10
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +9 -6
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +8 -7
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +56 -2
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +44 -0
- package/node_modules/@llblab/pi-telegram/package.json +2 -1
- package/node_modules/@llblab/pi-telegram/scripts/run-macos-tests.mjs +43 -0
- 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
|
|
6363
|
-
...(prepared.message ? { message: prepared.message.message_thread_id ===
|
|
6364
|
-
: withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id:
|
|
6365
|
-
...(prepared.edited_message ? { edited_message: prepared.edited_message.message_thread_id ===
|
|
6366
|
-
: withoutTelegramTurnReplyAnchor({ ...prepared.edited_message, message_thread_id:
|
|
6367
|
-
}
|
|
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
|
-
|
|
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
|
|
198
|
-
|
|
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
|
+
"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",
|