@llblab/pi-kit 0.26.0 → 0.27.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.
- package/BACKLOG.md +2 -2
- package/CHANGELOG.md +5 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +12 -5
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +9 -0
- package/node_modules/@llblab/pi-state-flow/README.md +116 -34
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +24 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +80 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +40 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +149 -298
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +58 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.d.ts +37 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +59 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +31 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +117 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +28 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +14 -6
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/README.md +19 -9
- package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +4 -4
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +644 -91
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +116 -37
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +118 -21
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +68 -8
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +88 -14
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +83 -66
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +391 -62
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +317 -62
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +85 -1
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +47 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +161 -295
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +63 -0
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +75 -0
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +120 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +7 -8
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +1 -2
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +5 -1
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +14 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +16 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +7 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +12 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +137 -83
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +26 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +57 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +109 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +3 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +31 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +17 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +48 -12
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -5
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +17 -10
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +8 -2
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +135 -97
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +25 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +140 -4
- package/node_modules/@llblab/pi-telegram/lib/model.ts +2 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +30 -1
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +21 -3
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +46 -13
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
|
@@ -371,7 +371,7 @@ export declare function createTelegramBusAgentMessageClient(deps: TelegramBusFol
|
|
|
371
371
|
routeMessage: (message: TelegramBusAgentMessage) => Promise<void>;
|
|
372
372
|
};
|
|
373
373
|
export declare function createTelegramBusFollowerApiCaller(deps: TelegramBusFollowerApiCallerDeps): (method: string, args: unknown[]) => Promise<unknown>;
|
|
374
|
-
export declare function createTelegramBusFollowerSessionReplacementSuspender(deps: TelegramBusFollowerSessionReplacementSuspenderDeps): () => Promise<void>;
|
|
374
|
+
export declare function createTelegramBusFollowerSessionReplacementSuspender(deps: TelegramBusFollowerSessionReplacementSuspenderDeps): (preserveTarget?: boolean) => Promise<void>;
|
|
375
375
|
export declare function createTelegramBusFollowerSessionRefreshHook<TContext>(deps: TelegramBusFollowerSessionRefreshHookDeps<TContext>): (_event: unknown, ctx: TContext) => Promise<void>;
|
|
376
376
|
export declare function createTelegramBusFollowerControlState(): TelegramBusFollowerControlState;
|
|
377
377
|
export declare function createTelegramBusFollowerRegistrationState(options?: {
|
|
@@ -519,7 +519,13 @@ function isTelegramStaleContextError(error) {
|
|
|
519
519
|
export function createTelegramBusFollowerSessionReplacementSuspender(deps) {
|
|
520
520
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
521
521
|
const getPid = deps.getPid ?? (() => process.pid);
|
|
522
|
-
return async () => {
|
|
522
|
+
return async (preserveTarget = true) => {
|
|
523
|
+
if (!preserveTarget) {
|
|
524
|
+
setTelegramFollowerSessionHandoff(undefined);
|
|
525
|
+
Threads.setTelegramLeaderSessionHandoff(undefined);
|
|
526
|
+
await deps.suspendPolling();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
523
529
|
const target = deps.registrationState.getTarget();
|
|
524
530
|
if (deps.registrationState.isRegistered() && target) {
|
|
525
531
|
setTelegramFollowerSessionHandoff({
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { type TelegramConfigStore } from "./config.ts";
|
|
7
7
|
import type * as Pi from "./pi.ts";
|
|
8
8
|
import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
|
|
9
|
-
import type
|
|
9
|
+
import { type TelegramBridgeStatusLineOptions } from "./status.ts";
|
|
10
10
|
import type { TelegramSessionReplacementIntent } from "./threads.ts";
|
|
11
11
|
import { type PendingTelegramControlItem, type TelegramQueueAdmissionReceipt } from "./queue.ts";
|
|
12
12
|
export interface ParsedTelegramCommand {
|
|
@@ -112,12 +112,21 @@ export interface TelegramBridgeCommandRegistrationDeps {
|
|
|
112
112
|
startPolling: (ctx: ExtensionCommandContext, options?: TelegramBridgeCommandStartPollingOptions) => void | Promise<void | TelegramBridgeCommandStartPollingResult> | TelegramBridgeCommandStartPollingResult;
|
|
113
113
|
stopPolling: () => Promise<void | string>;
|
|
114
114
|
recoverPollingStart?: (error: unknown) => Promise<TelegramPollingStartRecoveryResult>;
|
|
115
|
+
recordConnectionEvent?: (error: unknown, phase: string) => void;
|
|
115
116
|
getDisconnectThreadName?: () => string | undefined;
|
|
116
117
|
queueAgentConnectionContext?: (connected: boolean) => void;
|
|
117
118
|
updateStatus: (ctx: ExtensionCommandContext) => void;
|
|
119
|
+
isContextCurrent?: (ctx: ExtensionCommandContext) => boolean;
|
|
120
|
+
getSessionGeneration?: () => number;
|
|
121
|
+
connectionIntent?: {
|
|
122
|
+
begin(cwd: string, profileName?: string): string;
|
|
123
|
+
finish(id: string): void;
|
|
124
|
+
isActive(id: string): boolean;
|
|
125
|
+
cancel(): void;
|
|
126
|
+
};
|
|
118
127
|
getProfileNames?: () => string[];
|
|
119
|
-
activateDefaultProfileConfig?: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
120
|
-
activateProfileConfig?: (ctx: ExtensionCommandContext, profileName: string) => Promise<boolean>;
|
|
128
|
+
activateDefaultProfileConfig?: (ctx: ExtensionCommandContext, isCurrent: () => boolean) => Promise<void>;
|
|
129
|
+
activateProfileConfig?: (ctx: ExtensionCommandContext, profileName: string, isCurrent: () => boolean) => Promise<boolean>;
|
|
121
130
|
}
|
|
122
131
|
export type TelegramThreadDisplayNameRenamePort = (target: {
|
|
123
132
|
chatId: number;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { pairTelegramUserIfNeeded, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./config.js";
|
|
8
8
|
import { escapeHtml } from "./rendering.js";
|
|
9
|
+
import { formatTelegramConnectionFailure } from "./status.js";
|
|
9
10
|
import { createTelegramControlItemBuilder, createTelegramControlQueueController, createTelegramQueueAdmissionReceipt, } from "./queue.js";
|
|
10
11
|
const TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY = "__piTelegramCommandRegistry__";
|
|
11
12
|
const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
|
|
@@ -270,121 +271,174 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
270
271
|
pi.registerCommand("telegram-connect", {
|
|
271
272
|
description: "<profile> — Start Telegram bridge",
|
|
272
273
|
handler: async (args, ctx) => {
|
|
274
|
+
const sessionGeneration = deps.getSessionGeneration?.();
|
|
275
|
+
let intentId;
|
|
276
|
+
// Pi context getters throw after replacement; check plain intent/generation first.
|
|
277
|
+
const isCurrent = () => (!intentId || deps.connectionIntent?.isActive(intentId) !== false) &&
|
|
278
|
+
(sessionGeneration === undefined || deps.getSessionGeneration?.() === sessionGeneration) &&
|
|
279
|
+
deps.isContextCurrent?.(ctx) !== false;
|
|
280
|
+
if (!isCurrent())
|
|
281
|
+
return;
|
|
273
282
|
if (args.trim().split(/\s+/).some((word) => /^as=/i.test(word))) {
|
|
274
283
|
ctx.ui.notify("Thread names are configured from Telegram, not from Pi commands.", "warning");
|
|
275
284
|
deps.updateStatus(ctx);
|
|
276
285
|
return;
|
|
277
286
|
}
|
|
278
287
|
const profileName = parseTelegramProfileArg(args);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (!deps.hasBotToken()) {
|
|
292
|
-
const botTokenDiagnostic = deps.getBotTokenDiagnostic?.();
|
|
293
|
-
if (botTokenDiagnostic)
|
|
294
|
-
ctx.ui.notify(botTokenDiagnostic, "error");
|
|
295
|
-
const profileNames = deps.getProfileNames?.() ?? [];
|
|
296
|
-
if (!profileName && profileNames.length > 0) {
|
|
297
|
-
ctx.ui.notify(`No default Telegram profile configured. Available profiles: ${profileNames.join(", ")}. Use /telegram-connect <profileName> or /telegram-setup to create a default profile.`, "info");
|
|
298
|
-
deps.updateStatus(ctx);
|
|
299
|
-
return;
|
|
288
|
+
intentId = deps.connectionIntent?.begin(ctx.cwd, profileName);
|
|
289
|
+
try {
|
|
290
|
+
if (profileName && deps.activateProfileConfig) {
|
|
291
|
+
const ok = await deps.activateProfileConfig(ctx, profileName, isCurrent);
|
|
292
|
+
if (!isCurrent())
|
|
293
|
+
return;
|
|
294
|
+
if (!ok) {
|
|
295
|
+
ctx.ui.notify(`Profile "${profileName}" not found.`, "error");
|
|
296
|
+
deps.updateStatus(ctx);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
ctx.ui.notify(`Activated profile "${profileName}".`, "info");
|
|
300
300
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const startWithRecovery = async (options) => {
|
|
306
|
-
try {
|
|
307
|
-
return await deps.startPolling(ctx, options);
|
|
301
|
+
else {
|
|
302
|
+
await (deps.activateDefaultProfileConfig?.(ctx, isCurrent) ?? deps.reloadConfig());
|
|
303
|
+
if (!isCurrent())
|
|
304
|
+
return;
|
|
308
305
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
306
|
+
if (!deps.hasBotToken()) {
|
|
307
|
+
const botTokenDiagnostic = deps.getBotTokenDiagnostic?.();
|
|
308
|
+
if (botTokenDiagnostic)
|
|
309
|
+
ctx.ui.notify(botTokenDiagnostic, "error");
|
|
310
|
+
const profileNames = deps.getProfileNames?.() ?? [];
|
|
311
|
+
if (!profileName && profileNames.length > 0) {
|
|
312
|
+
ctx.ui.notify(`No default Telegram profile configured. Available profiles: ${profileNames.join(", ")}. Use /telegram-connect <profileName> or /telegram-setup to create a default profile.`, "info");
|
|
313
|
+
deps.updateStatus(ctx);
|
|
314
|
+
return;
|
|
317
315
|
}
|
|
318
|
-
|
|
316
|
+
await deps.promptForConfig(ctx, profileName);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
let recoveryUsed = false;
|
|
320
|
+
const startWithRecovery = async (options) => {
|
|
319
321
|
try {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
return await deps.startPolling(ctx, options);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
if (!isCurrent())
|
|
326
|
+
return;
|
|
327
|
+
if (!deps.recoverPollingStart || recoveryUsed)
|
|
328
|
+
throw error;
|
|
329
|
+
deps.recordConnectionEvent?.(error, "polling-start");
|
|
330
|
+
const recovery = await deps.recoverPollingStart(error);
|
|
331
|
+
if (!isCurrent())
|
|
332
|
+
return;
|
|
333
|
+
if (recovery.kind === "unhandled")
|
|
334
|
+
throw error;
|
|
335
|
+
if (recovery.kind === "blocked") {
|
|
336
|
+
return { ok: false, message: recovery.message,
|
|
337
|
+
notice: "Telegram recovery blocked. Check /telegram-status --debug." };
|
|
338
|
+
}
|
|
339
|
+
recoveryUsed = true;
|
|
340
|
+
deps.recordConnectionEvent?.(recovery.message, "recovery");
|
|
341
|
+
try {
|
|
342
|
+
const retry = await deps.startPolling(ctx, options);
|
|
343
|
+
if (!isCurrent())
|
|
344
|
+
return;
|
|
345
|
+
if (!retry)
|
|
346
|
+
return { ok: true, message: "Telegram bridge connected; temporary state recovered." };
|
|
347
|
+
return {
|
|
348
|
+
...retry,
|
|
349
|
+
message: retry.ok
|
|
350
|
+
? "Telegram bridge connected; temporary state recovered."
|
|
351
|
+
: retry.message,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (!isCurrent())
|
|
356
|
+
return;
|
|
357
|
+
deps.recordConnectionEvent?.(error, "recovery-retry");
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
notice: "Telegram recovery failed. Restart this Pi instance.",
|
|
361
|
+
};
|
|
323
362
|
}
|
|
324
|
-
return {
|
|
325
|
-
...retry,
|
|
326
|
-
message: retry.ok
|
|
327
|
-
? `${recovery.message} ${retry.message ?? "Telegram bridge connected."}`
|
|
328
|
-
: retry.message,
|
|
329
|
-
};
|
|
330
363
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
364
|
+
};
|
|
365
|
+
let result = await startWithRecovery({ forceFreshLeaderThread: true });
|
|
366
|
+
if (!isCurrent())
|
|
367
|
+
return;
|
|
368
|
+
if (result && !result.ok && result.canTakeover) {
|
|
369
|
+
const confirmed = await ctx.ui.confirm(formatTelegramTakeoverTitle(ctx), formatTelegramTakeoverPrompt(ctx, result.owner));
|
|
370
|
+
if (!isCurrent())
|
|
371
|
+
return;
|
|
372
|
+
if (!confirmed) {
|
|
373
|
+
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
374
|
+
deps.updateStatus(ctx);
|
|
375
|
+
return;
|
|
336
376
|
}
|
|
377
|
+
result = await startWithRecovery({ force: true, forceFreshLeaderThread: true });
|
|
378
|
+
if (!isCurrent())
|
|
379
|
+
return;
|
|
337
380
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (result && !result.ok && result.canTakeover) {
|
|
343
|
-
const confirmed = await ctx.ui.confirm(formatTelegramTakeoverTitle(ctx), formatTelegramTakeoverPrompt(ctx, result.owner));
|
|
344
|
-
if (!confirmed) {
|
|
345
|
-
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
346
|
-
deps.updateStatus(ctx);
|
|
347
|
-
return;
|
|
381
|
+
if (result && !result.ok) {
|
|
382
|
+
if (result.message)
|
|
383
|
+
deps.recordConnectionEvent?.(result.message, "connect-refused");
|
|
384
|
+
ctx.ui.notify(result.notice ?? formatTelegramConnectionFailure(result.message), "warning");
|
|
348
385
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
386
|
+
else if (result?.message) {
|
|
387
|
+
ctx.ui.notify(result.message, "info");
|
|
388
|
+
}
|
|
389
|
+
if (!result || result.ok)
|
|
390
|
+
deps.queueAgentConnectionContext?.(true);
|
|
391
|
+
deps.updateStatus(ctx);
|
|
353
392
|
}
|
|
354
|
-
|
|
355
|
-
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (!isCurrent())
|
|
395
|
+
return;
|
|
396
|
+
deps.recordConnectionEvent?.(error, "connect");
|
|
397
|
+
ctx.ui.notify(formatTelegramConnectionFailure(error), "warning");
|
|
398
|
+
deps.updateStatus(ctx);
|
|
356
399
|
}
|
|
357
|
-
|
|
358
|
-
|
|
400
|
+
finally {
|
|
401
|
+
if (intentId)
|
|
402
|
+
deps.connectionIntent?.finish(intentId);
|
|
359
403
|
}
|
|
360
|
-
deps.updateStatus(ctx);
|
|
361
404
|
},
|
|
362
405
|
});
|
|
363
406
|
pi.registerCommand("telegram-disconnect", {
|
|
364
407
|
description: "Stop Telegram and delete current thread in Threaded Mode",
|
|
365
408
|
handler: async (_args, ctx) => {
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
409
|
+
const generation = deps.getSessionGeneration?.();
|
|
410
|
+
const isCurrent = () => (generation === undefined || deps.getSessionGeneration?.() === generation) &&
|
|
411
|
+
deps.isContextCurrent?.(ctx) !== false;
|
|
412
|
+
if (!isCurrent())
|
|
413
|
+
return;
|
|
414
|
+
deps.connectionIntent?.cancel();
|
|
375
415
|
try {
|
|
416
|
+
const threadName = deps.getDisconnectThreadName?.();
|
|
417
|
+
if (threadName) {
|
|
418
|
+
const confirmed = await ctx.ui.confirm(ctx.ui.theme.fg("accent", "pi-telegram"), `Delete Telegram thread ${ctx.ui.theme.fg("warning", threadName)} and disconnect this Pi session?`);
|
|
419
|
+
if (!isCurrent())
|
|
420
|
+
return;
|
|
421
|
+
if (!confirmed) {
|
|
422
|
+
ctx.ui.notify("Telegram disconnect cancelled.", "info");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
376
426
|
const message = await deps.stopPolling();
|
|
427
|
+
if (!isCurrent())
|
|
428
|
+
return;
|
|
377
429
|
if (message)
|
|
378
430
|
ctx.ui.notify(message, "info");
|
|
379
431
|
deps.queueAgentConnectionContext?.(false);
|
|
380
432
|
}
|
|
381
433
|
catch (error) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
434
|
+
deps.recordConnectionEvent?.(error, "disconnect");
|
|
435
|
+
if (!isCurrent())
|
|
436
|
+
return;
|
|
437
|
+
ctx.ui.notify("Telegram disconnect incomplete; keep Pi open. Check /telegram-status --debug.", "warning");
|
|
385
438
|
}
|
|
386
439
|
finally {
|
|
387
|
-
|
|
440
|
+
if (isCurrent())
|
|
441
|
+
deps.updateStatus(ctx);
|
|
388
442
|
}
|
|
389
443
|
},
|
|
390
444
|
});
|
|
@@ -1361,6 +1361,28 @@ export default function (pi) {
|
|
|
1361
1361
|
resolveAutomaticThreadCleanupEnabled: configControls.resolveAutomaticThreadCleanupEnabled,
|
|
1362
1362
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1363
1363
|
});
|
|
1364
|
+
const connectionIntent = Lifecycle.createTelegramConnectionIntentRuntime();
|
|
1365
|
+
const connectionLifecycle = Lifecycle.createTelegramConnectionLifecycle({
|
|
1366
|
+
intent: connectionIntent,
|
|
1367
|
+
getGeneration: telegramSessionContextStore.getGeneration,
|
|
1368
|
+
isCurrent: telegramSessionContextStore.isCurrent,
|
|
1369
|
+
getProfileName: configStore.getActiveProfileName,
|
|
1370
|
+
isConnected() {
|
|
1371
|
+
return lockRuntime.owns() || telegramBusFollowerRegistrationState.isRegistered();
|
|
1372
|
+
},
|
|
1373
|
+
async activateProfile(profileName, isCurrent) {
|
|
1374
|
+
await configStore.load();
|
|
1375
|
+
if (!isCurrent())
|
|
1376
|
+
return false;
|
|
1377
|
+
return configStore.activateProfile(profileName) && configStore.hasBotToken();
|
|
1378
|
+
},
|
|
1379
|
+
start(ctx) {
|
|
1380
|
+
return lockedPollingRuntime.start(ctx);
|
|
1381
|
+
},
|
|
1382
|
+
recordError(error) {
|
|
1383
|
+
recordRuntimeEvent("connection", error, { phase: "resume-connect" });
|
|
1384
|
+
},
|
|
1385
|
+
});
|
|
1364
1386
|
const telegramBridgeSessionLifecycleDeps = Lifecycle.createTelegramBridgeSessionLifecycleDeps({
|
|
1365
1387
|
contextStore: telegramSessionContextStore,
|
|
1366
1388
|
queue: {
|
|
@@ -1408,6 +1430,7 @@ export default function (pi) {
|
|
|
1408
1430
|
},
|
|
1409
1431
|
delivery: deliveryLifecycleRuntime,
|
|
1410
1432
|
polling: lockedPollingRuntime,
|
|
1433
|
+
connection: connectionLifecycle,
|
|
1411
1434
|
inboundWorker: {
|
|
1412
1435
|
onSessionShutdown: updateAdmissionRuntimeBinding.onSessionShutdown,
|
|
1413
1436
|
},
|
|
@@ -1536,6 +1559,9 @@ export default function (pi) {
|
|
|
1536
1559
|
modelContextAvailabilityRuntime.reconcile();
|
|
1537
1560
|
},
|
|
1538
1561
|
getStatusLines,
|
|
1562
|
+
isContextCurrent: telegramSessionContextStore.isCurrent,
|
|
1563
|
+
getSessionGeneration: telegramSessionContextStore.getGeneration,
|
|
1564
|
+
connectionIntent,
|
|
1539
1565
|
buttonActionStore,
|
|
1540
1566
|
sendMarkdownReply,
|
|
1541
1567
|
async sendChannelMarkdownMessage(channel, markdown, options) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram lifecycle hook registration
|
|
2
|
+
* Telegram session lifecycle coordination and hook registration
|
|
3
3
|
* Zones: pi agent lifecycle, telegram session
|
|
4
|
-
*
|
|
4
|
+
* Owns context generations, bounded connect intent across resume, and session sequencing.
|
|
5
|
+
* Transport authority, durable bindings, and queue custody remain with their owners.
|
|
5
6
|
*/
|
|
6
7
|
import * as BusFollower from "./bus-follower.ts";
|
|
7
8
|
import * as Queue from "./queue.ts";
|
|
@@ -50,6 +51,58 @@ export interface TelegramSessionLifecycleHooks {
|
|
|
50
51
|
onSessionStart: (event: SessionStartEvent, ctx: ExtensionContext) => Promise<void>;
|
|
51
52
|
onSessionShutdown: (event: SessionShutdownEvent, ctx: ExtensionContext) => Promise<void>;
|
|
52
53
|
}
|
|
54
|
+
export interface TelegramConnectionIntent {
|
|
55
|
+
id: string;
|
|
56
|
+
cwd: string;
|
|
57
|
+
profileName?: string;
|
|
58
|
+
}
|
|
59
|
+
interface TelegramConnectionHandoff extends TelegramConnectionIntent {
|
|
60
|
+
pid: number;
|
|
61
|
+
targetSessionFile: string;
|
|
62
|
+
expiresAtMs: number;
|
|
63
|
+
}
|
|
64
|
+
export interface TelegramConnectionHandoffStore {
|
|
65
|
+
pending?: TelegramConnectionHandoff;
|
|
66
|
+
}
|
|
67
|
+
export interface TelegramConnectionLifecycle {
|
|
68
|
+
prepare(event: SessionStartEvent, ctx: ExtensionContext): (() => void) | undefined;
|
|
69
|
+
onSessionShutdown(event: SessionShutdownEvent, ctx: ExtensionContext): void;
|
|
70
|
+
}
|
|
71
|
+
export declare function createTelegramConnectionLifecycle(deps: {
|
|
72
|
+
intent: ReturnType<typeof createTelegramConnectionIntentRuntime>;
|
|
73
|
+
getGeneration(): number;
|
|
74
|
+
isCurrent(ctx: ExtensionContext): boolean;
|
|
75
|
+
getProfileName(): string | undefined;
|
|
76
|
+
isConnected(): boolean;
|
|
77
|
+
activateProfile(profileName: string | undefined, isCurrent: () => boolean): Promise<boolean>;
|
|
78
|
+
start(ctx: ExtensionContext): Promise<{
|
|
79
|
+
ok: boolean;
|
|
80
|
+
message?: string;
|
|
81
|
+
}>;
|
|
82
|
+
recordError(error: unknown): void;
|
|
83
|
+
}): TelegramConnectionLifecycle;
|
|
84
|
+
export declare function createTelegramConnectionIntentRuntime(options?: {
|
|
85
|
+
store?: TelegramConnectionHandoffStore;
|
|
86
|
+
now?: () => number;
|
|
87
|
+
pid?: number;
|
|
88
|
+
}): {
|
|
89
|
+
begin(cwd: string, profileName?: string): string;
|
|
90
|
+
isActive(id: string): boolean;
|
|
91
|
+
finish(id: string): void;
|
|
92
|
+
cancel(): void;
|
|
93
|
+
suspend(input: {
|
|
94
|
+
reason: string;
|
|
95
|
+
cwd: string;
|
|
96
|
+
targetSessionFile?: string;
|
|
97
|
+
connected: boolean;
|
|
98
|
+
profileName?: string;
|
|
99
|
+
}): void;
|
|
100
|
+
resume(input: {
|
|
101
|
+
reason: string;
|
|
102
|
+
cwd: string;
|
|
103
|
+
sessionFile?: string;
|
|
104
|
+
}): TelegramConnectionIntent | undefined;
|
|
105
|
+
};
|
|
53
106
|
export interface TelegramSessionContextStore<TContext> {
|
|
54
107
|
get: () => TContext | undefined;
|
|
55
108
|
getGeneration: () => number;
|
|
@@ -62,6 +115,7 @@ export declare function createTelegramSessionContextStore<TContext>(options?: {
|
|
|
62
115
|
}): TelegramSessionContextStore<TContext>;
|
|
63
116
|
export declare function createTelegramSessionGenerationFence(store: TelegramSessionContextStore<ExtensionContext>, hooks: TelegramSessionLifecycleHooks): TelegramSessionLifecycleHooks;
|
|
64
117
|
export interface TelegramBridgeSessionServiceRuntime {
|
|
118
|
+
connection?: TelegramConnectionLifecycle;
|
|
65
119
|
resumeGroupedInput(ctx: ExtensionContext): void;
|
|
66
120
|
suspendGroupedInput(): void;
|
|
67
121
|
delivery: {
|
|
@@ -108,6 +162,7 @@ export interface TelegramBridgeSessionLifecyclePorts<TQueueItem, TModel = unknow
|
|
|
108
162
|
};
|
|
109
163
|
delivery: TelegramBridgeSessionServiceRuntime["delivery"];
|
|
110
164
|
polling: TelegramBridgeSessionServiceRuntime["polling"];
|
|
165
|
+
connection?: TelegramBridgeSessionServiceRuntime["connection"];
|
|
111
166
|
inboundWorker: TelegramBridgeSessionServiceRuntime["inboundWorker"];
|
|
112
167
|
capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
|
|
113
168
|
queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram lifecycle hook registration
|
|
2
|
+
* Telegram session lifecycle coordination and hook registration
|
|
3
3
|
* Zones: pi agent lifecycle, telegram session
|
|
4
|
-
*
|
|
4
|
+
* Owns context generations, bounded connect intent across resume, and session sequencing.
|
|
5
|
+
* Transport authority, durable bindings, and queue custody remain with their owners.
|
|
5
6
|
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { formatTelegramConnectionFailure } from "./status.js";
|
|
6
9
|
import * as BusFollower from "./bus-follower.js";
|
|
7
10
|
import * as Queue from "./queue.js";
|
|
8
11
|
import * as TextGroups from "./text-groups.js";
|
|
@@ -23,6 +26,98 @@ export function createAgentStartDedupHook(inner, schedulePublication) {
|
|
|
23
26
|
return inner(event, ctx);
|
|
24
27
|
};
|
|
25
28
|
}
|
|
29
|
+
const HANDOFF_KEY = Symbol.for("pi-telegram.connection-resume.v1");
|
|
30
|
+
function processHandoffStore() {
|
|
31
|
+
const globals = globalThis;
|
|
32
|
+
return globals[HANDOFF_KEY] ??= {};
|
|
33
|
+
}
|
|
34
|
+
export function createTelegramConnectionLifecycle(deps) {
|
|
35
|
+
return {
|
|
36
|
+
onSessionShutdown(event, ctx) {
|
|
37
|
+
deps.intent.suspend({ reason: event.reason, cwd: ctx.cwd,
|
|
38
|
+
targetSessionFile: event.targetSessionFile, connected: deps.isConnected(),
|
|
39
|
+
profileName: deps.getProfileName() });
|
|
40
|
+
},
|
|
41
|
+
prepare(event, ctx) {
|
|
42
|
+
const intent = deps.intent.resume({ reason: event.reason, cwd: ctx.cwd,
|
|
43
|
+
sessionFile: event.reason === "resume" ? ctx.sessionManager.getSessionFile() : undefined });
|
|
44
|
+
if (!intent)
|
|
45
|
+
return undefined;
|
|
46
|
+
const generation = deps.getGeneration();
|
|
47
|
+
const isCurrent = () => deps.intent.isActive(intent.id) &&
|
|
48
|
+
generation === deps.getGeneration() && deps.isCurrent(ctx);
|
|
49
|
+
return () => {
|
|
50
|
+
// Startup remains extension-owned background work; no captured command ctx crosses resume.
|
|
51
|
+
void (async () => {
|
|
52
|
+
if (!isCurrent())
|
|
53
|
+
return;
|
|
54
|
+
if (!await deps.activateProfile(intent.profileName, isCurrent)) {
|
|
55
|
+
if (isCurrent())
|
|
56
|
+
ctx.ui.notify("Telegram profile unavailable. Run /telegram-setup.", "warning");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!isCurrent())
|
|
60
|
+
return;
|
|
61
|
+
const result = await deps.start(ctx);
|
|
62
|
+
if (!isCurrent())
|
|
63
|
+
return;
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
deps.recordError(new Error(result.message ?? "Telegram resume connection failed."));
|
|
66
|
+
ctx.ui.notify(formatTelegramConnectionFailure(result.message), "warning");
|
|
67
|
+
}
|
|
68
|
+
})().catch((error) => {
|
|
69
|
+
deps.recordError(error);
|
|
70
|
+
if (isCurrent())
|
|
71
|
+
ctx.ui.notify(formatTelegramConnectionFailure(error), "warning");
|
|
72
|
+
}).finally(() => deps.intent.finish(intent.id));
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function createTelegramConnectionIntentRuntime(options = {}) {
|
|
78
|
+
const store = options.store ?? processHandoffStore();
|
|
79
|
+
const now = options.now ?? Date.now;
|
|
80
|
+
const pid = options.pid ?? process.pid;
|
|
81
|
+
let active;
|
|
82
|
+
return {
|
|
83
|
+
begin(cwd, profileName) {
|
|
84
|
+
store.pending = undefined;
|
|
85
|
+
active = { id: randomUUID(), cwd, profileName };
|
|
86
|
+
return active.id;
|
|
87
|
+
},
|
|
88
|
+
isActive(id) {
|
|
89
|
+
return active?.id === id;
|
|
90
|
+
},
|
|
91
|
+
finish(id) {
|
|
92
|
+
if (active?.id === id)
|
|
93
|
+
active = undefined;
|
|
94
|
+
},
|
|
95
|
+
cancel() {
|
|
96
|
+
active = undefined;
|
|
97
|
+
store.pending = undefined;
|
|
98
|
+
},
|
|
99
|
+
suspend(input) {
|
|
100
|
+
const intent = active ?? (input.connected
|
|
101
|
+
? { id: randomUUID(), cwd: input.cwd, profileName: input.profileName }
|
|
102
|
+
: undefined);
|
|
103
|
+
active = undefined;
|
|
104
|
+
store.pending = input.reason === "resume" && input.targetSessionFile &&
|
|
105
|
+
intent?.cwd === input.cwd
|
|
106
|
+
? { ...intent, pid, targetSessionFile: input.targetSessionFile, expiresAtMs: now() + 30_000 }
|
|
107
|
+
: undefined;
|
|
108
|
+
},
|
|
109
|
+
resume(input) {
|
|
110
|
+
const handoff = store.pending;
|
|
111
|
+
store.pending = undefined;
|
|
112
|
+
if (!handoff || handoff.pid !== pid || now() >= handoff.expiresAtMs ||
|
|
113
|
+
input.reason !== "resume" || input.cwd !== handoff.cwd ||
|
|
114
|
+
input.sessionFile !== handoff.targetSessionFile)
|
|
115
|
+
return undefined;
|
|
116
|
+
active = { id: randomUUID(), cwd: handoff.cwd, profileName: handoff.profileName };
|
|
117
|
+
return { ...active };
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
26
121
|
export function createTelegramSessionContextStore(options = {}) {
|
|
27
122
|
let currentContext;
|
|
28
123
|
let currentIdentity;
|
|
@@ -112,6 +207,7 @@ export function createTelegramBridgeSessionLifecycleDeps(ports) {
|
|
|
112
207
|
}),
|
|
113
208
|
delivery: ports.services.delivery,
|
|
114
209
|
polling: ports.services.polling,
|
|
210
|
+
connection: ports.services.connection,
|
|
115
211
|
inboundWorker: ports.services.inboundWorker,
|
|
116
212
|
capabilityMonitor: ports.services.capabilityMonitor,
|
|
117
213
|
queueWatchdog: ports.services.queueWatchdog,
|
|
@@ -128,20 +224,27 @@ export function createTelegramBridgeSessionLifecycleAssembly(deps) {
|
|
|
128
224
|
suspendPolling: deps.follower.suspendPolling,
|
|
129
225
|
recordRuntimeEvent: deps.follower.recordRuntimeEvent,
|
|
130
226
|
});
|
|
227
|
+
let preserveTarget = true;
|
|
131
228
|
const queueLifecycle = Queue.createTelegramSessionLifecycleRuntime({
|
|
132
229
|
...deps.queue,
|
|
133
230
|
isSessionActive,
|
|
134
|
-
stopPolling: suspendForReplacement,
|
|
231
|
+
stopPolling: () => suspendForReplacement(preserveTarget),
|
|
135
232
|
clearPendingMediaGroups: deps.services.suspendGroupedInput,
|
|
136
233
|
});
|
|
137
234
|
const servicesLifecycle = {
|
|
138
235
|
async onSessionStart(event, ctx) {
|
|
236
|
+
const resumeConnection = deps.services.connection?.prepare(event, ctx);
|
|
139
237
|
await queueLifecycle.onSessionStart(event, ctx);
|
|
140
238
|
if (!isSessionActive(ctx))
|
|
141
239
|
return;
|
|
142
240
|
deps.services.resumeGroupedInput(ctx);
|
|
143
241
|
await deps.services.delivery.onSessionStart();
|
|
144
|
-
|
|
242
|
+
if (!isSessionActive(ctx))
|
|
243
|
+
return;
|
|
244
|
+
if (resumeConnection)
|
|
245
|
+
resumeConnection();
|
|
246
|
+
else
|
|
247
|
+
await deps.services.polling.onSessionStart(event, ctx);
|
|
145
248
|
deps.services.capabilityMonitor.start(ctx);
|
|
146
249
|
deps.services.queueWatchdog.start(ctx);
|
|
147
250
|
},
|
|
@@ -150,6 +253,8 @@ export function createTelegramBridgeSessionLifecycleAssembly(deps) {
|
|
|
150
253
|
const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
|
|
151
254
|
if (!isCurrent())
|
|
152
255
|
return;
|
|
256
|
+
deps.services.connection?.onSessionShutdown(event, ctx);
|
|
257
|
+
preserveTarget = event.reason !== "resume";
|
|
153
258
|
let preserveThread;
|
|
154
259
|
if (event.reason === "quit") {
|
|
155
260
|
try {
|
|
@@ -294,10 +294,8 @@ function truncateTelegramModelSwitchStatusSummary(text, maxWords = 4, maxLength
|
|
|
294
294
|
}
|
|
295
295
|
export function buildTelegramModelSwitchContinuationText(telegramPrefix, model, thinkingLevel) {
|
|
296
296
|
const modelLabel = `${model.provider}/${model.id}`;
|
|
297
|
-
const thinkingSuffix = thinkingLevel
|
|
298
|
-
|
|
299
|
-
: "";
|
|
300
|
-
return `${telegramPrefix} Continue the interrupted previous request using the newly selected model (${modelLabel}). Resume from the last unfinished step instead of restarting from scratch unless necessary.${thinkingSuffix}`;
|
|
297
|
+
const thinkingSuffix = thinkingLevel ? `; thinking: ${thinkingLevel}` : "";
|
|
298
|
+
return `${telegramPrefix} Continue from the last unfinished step. Model: ${modelLabel}${thinkingSuffix}.`;
|
|
301
299
|
}
|
|
302
300
|
export function buildTelegramModelSwitchContinuationTurn(options) {
|
|
303
301
|
const modelLabel = `${options.selection.model.provider}/${options.selection.model.id}`;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram status rendering helpers
|
|
3
3
|
* Zones: telegram ui, pi agent diagnostics, tui
|
|
4
|
-
*
|
|
4
|
+
* Owns status summaries, redacted runtime diagnostics, and compact connection-failure copy
|
|
5
5
|
*/
|
|
6
|
+
/** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
|
|
7
|
+
export declare function formatTelegramConnectionFailure(error: unknown): string;
|
|
6
8
|
export type TelegramStatusQueueLane = "control" | "priority" | "default";
|
|
7
9
|
export interface TelegramUsageStats {
|
|
8
10
|
totalInput: number;
|