@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
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import type * as Pi from "./pi.ts";
|
|
15
15
|
import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
|
|
16
16
|
import { escapeHtml } from "./rendering.ts";
|
|
17
|
-
import type
|
|
17
|
+
import { formatTelegramConnectionFailure, type TelegramBridgeStatusLineOptions } from "./status.ts";
|
|
18
18
|
import type { TelegramSessionReplacementIntent } from "./threads.ts";
|
|
19
19
|
import {
|
|
20
20
|
createTelegramControlItemBuilder,
|
|
@@ -399,14 +399,24 @@ export interface TelegramBridgeCommandRegistrationDeps {
|
|
|
399
399
|
recoverPollingStart?: (
|
|
400
400
|
error: unknown,
|
|
401
401
|
) => Promise<TelegramPollingStartRecoveryResult>;
|
|
402
|
+
recordConnectionEvent?: (error: unknown, phase: string) => void;
|
|
402
403
|
getDisconnectThreadName?: () => string | undefined;
|
|
403
404
|
queueAgentConnectionContext?: (connected: boolean) => void;
|
|
404
405
|
updateStatus: (ctx: ExtensionCommandContext) => void;
|
|
406
|
+
isContextCurrent?: (ctx: ExtensionCommandContext) => boolean;
|
|
407
|
+
getSessionGeneration?: () => number;
|
|
408
|
+
connectionIntent?: {
|
|
409
|
+
begin(cwd: string, profileName?: string): string;
|
|
410
|
+
finish(id: string): void;
|
|
411
|
+
isActive(id: string): boolean;
|
|
412
|
+
cancel(): void;
|
|
413
|
+
};
|
|
405
414
|
getProfileNames?: () => string[];
|
|
406
|
-
activateDefaultProfileConfig?: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
415
|
+
activateDefaultProfileConfig?: (ctx: ExtensionCommandContext, isCurrent: () => boolean) => Promise<void>;
|
|
407
416
|
activateProfileConfig?: (
|
|
408
417
|
ctx: ExtensionCommandContext,
|
|
409
418
|
profileName: string,
|
|
419
|
+
isCurrent: () => boolean,
|
|
410
420
|
) => Promise<boolean>;
|
|
411
421
|
}
|
|
412
422
|
|
|
@@ -504,6 +514,14 @@ export function registerTelegramBridgeCommands(
|
|
|
504
514
|
pi.registerCommand("telegram-connect", {
|
|
505
515
|
description: "<profile> — Start Telegram bridge",
|
|
506
516
|
handler: async (args, ctx) => {
|
|
517
|
+
const sessionGeneration = deps.getSessionGeneration?.();
|
|
518
|
+
let intentId: string | undefined;
|
|
519
|
+
// Pi context getters throw after replacement; check plain intent/generation first.
|
|
520
|
+
const isCurrent = () =>
|
|
521
|
+
(!intentId || deps.connectionIntent?.isActive(intentId) !== false) &&
|
|
522
|
+
(sessionGeneration === undefined || deps.getSessionGeneration?.() === sessionGeneration) &&
|
|
523
|
+
deps.isContextCurrent?.(ctx) !== false;
|
|
524
|
+
if (!isCurrent()) return;
|
|
507
525
|
if (args.trim().split(/\s+/).some((word) => /^as=/i.test(word))) {
|
|
508
526
|
ctx.ui.notify(
|
|
509
527
|
"Thread names are configured from Telegram, not from Pi commands.",
|
|
@@ -513,121 +531,141 @@ export function registerTelegramBridgeCommands(
|
|
|
513
531
|
return;
|
|
514
532
|
}
|
|
515
533
|
const profileName = parseTelegramProfileArg(args);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
534
|
+
intentId = deps.connectionIntent?.begin(ctx.cwd, profileName);
|
|
535
|
+
try {
|
|
536
|
+
if (profileName && deps.activateProfileConfig) {
|
|
537
|
+
const ok = await deps.activateProfileConfig(ctx, profileName, isCurrent);
|
|
538
|
+
if (!isCurrent()) return;
|
|
539
|
+
if (!ok) {
|
|
540
|
+
ctx.ui.notify(`Profile "${profileName}" not found.`, "error");
|
|
541
|
+
deps.updateStatus(ctx);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
ctx.ui.notify(`Activated profile "${profileName}".`, "info");
|
|
545
|
+
} else {
|
|
546
|
+
await (deps.activateDefaultProfileConfig?.(ctx, isCurrent) ?? deps.reloadConfig());
|
|
547
|
+
if (!isCurrent()) return;
|
|
522
548
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
);
|
|
536
|
-
deps.updateStatus(ctx);
|
|
549
|
+
if (!deps.hasBotToken()) {
|
|
550
|
+
const botTokenDiagnostic = deps.getBotTokenDiagnostic?.();
|
|
551
|
+
if (botTokenDiagnostic) ctx.ui.notify(botTokenDiagnostic, "error");
|
|
552
|
+
const profileNames = deps.getProfileNames?.() ?? [];
|
|
553
|
+
if (!profileName && profileNames.length > 0) {
|
|
554
|
+
ctx.ui.notify(
|
|
555
|
+
`No default Telegram profile configured. Available profiles: ${profileNames.join(", ")}. Use /telegram-connect <profileName> or /telegram-setup to create a default profile.`,
|
|
556
|
+
"info",
|
|
557
|
+
);
|
|
558
|
+
deps.updateStatus(ctx);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
await deps.promptForConfig(ctx, profileName);
|
|
537
562
|
return;
|
|
538
563
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const startWithRecovery = async (
|
|
544
|
-
options: TelegramBridgeCommandStartPollingOptions,
|
|
545
|
-
): Promise<void | TelegramBridgeCommandStartPollingResult> => {
|
|
546
|
-
try {
|
|
547
|
-
return await deps.startPolling(ctx, options);
|
|
548
|
-
} catch (error) {
|
|
549
|
-
if (!deps.recoverPollingStart || recoveryUsed) throw error;
|
|
550
|
-
const recovery = await deps.recoverPollingStart(error);
|
|
551
|
-
if (recovery.kind === "unhandled") throw error;
|
|
552
|
-
if (recovery.kind === "blocked") {
|
|
553
|
-
return { ok: false, message: recovery.message };
|
|
554
|
-
}
|
|
555
|
-
recoveryUsed = true;
|
|
564
|
+
let recoveryUsed = false;
|
|
565
|
+
const startWithRecovery = async (
|
|
566
|
+
options: TelegramBridgeCommandStartPollingOptions,
|
|
567
|
+
): Promise<void | (TelegramBridgeCommandStartPollingResult & { notice?: string })> => {
|
|
556
568
|
try {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
569
|
+
return await deps.startPolling(ctx, options);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (!isCurrent()) return;
|
|
572
|
+
if (!deps.recoverPollingStart || recoveryUsed) throw error;
|
|
573
|
+
deps.recordConnectionEvent?.(error, "polling-start");
|
|
574
|
+
const recovery = await deps.recoverPollingStart(error);
|
|
575
|
+
if (!isCurrent()) return;
|
|
576
|
+
if (recovery.kind === "unhandled") throw error;
|
|
577
|
+
if (recovery.kind === "blocked") {
|
|
578
|
+
return { ok: false, message: recovery.message,
|
|
579
|
+
notice: "Telegram recovery blocked. Check /telegram-status --debug." };
|
|
580
|
+
}
|
|
581
|
+
recoveryUsed = true;
|
|
582
|
+
deps.recordConnectionEvent?.(recovery.message, "recovery");
|
|
583
|
+
try {
|
|
584
|
+
const retry = await deps.startPolling(ctx, options);
|
|
585
|
+
if (!isCurrent()) return;
|
|
586
|
+
if (!retry) return { ok: true, message: "Telegram bridge connected; temporary state recovered." };
|
|
587
|
+
return {
|
|
588
|
+
...retry,
|
|
589
|
+
message: retry.ok
|
|
590
|
+
? "Telegram bridge connected; temporary state recovered."
|
|
591
|
+
: retry.message,
|
|
592
|
+
};
|
|
593
|
+
} catch (error) {
|
|
594
|
+
if (!isCurrent()) return;
|
|
595
|
+
deps.recordConnectionEvent?.(error, "recovery-retry");
|
|
596
|
+
return {
|
|
597
|
+
ok: false,
|
|
598
|
+
notice: "Telegram recovery failed. Restart this Pi instance.",
|
|
599
|
+
};
|
|
560
600
|
}
|
|
561
|
-
return {
|
|
562
|
-
...retry,
|
|
563
|
-
message: retry.ok
|
|
564
|
-
? `${recovery.message} ${retry.message ?? "Telegram bridge connected."}`
|
|
565
|
-
: retry.message,
|
|
566
|
-
};
|
|
567
|
-
} catch {
|
|
568
|
-
return {
|
|
569
|
-
ok: false,
|
|
570
|
-
message:
|
|
571
|
-
"Telegram temporary state was recovered, but the bridge could not restart. Restart this Pi instance and run /telegram-connect again.",
|
|
572
|
-
};
|
|
573
601
|
}
|
|
602
|
+
};
|
|
603
|
+
let result = await startWithRecovery({ forceFreshLeaderThread: true });
|
|
604
|
+
if (!isCurrent()) return;
|
|
605
|
+
if (result && !result.ok && result.canTakeover) {
|
|
606
|
+
const confirmed = await ctx.ui.confirm(
|
|
607
|
+
formatTelegramTakeoverTitle(ctx),
|
|
608
|
+
formatTelegramTakeoverPrompt(ctx, result.owner),
|
|
609
|
+
);
|
|
610
|
+
if (!isCurrent()) return;
|
|
611
|
+
if (!confirmed) {
|
|
612
|
+
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
613
|
+
deps.updateStatus(ctx);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
result = await startWithRecovery({ force: true, forceFreshLeaderThread: true });
|
|
617
|
+
if (!isCurrent()) return;
|
|
574
618
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const confirmed = await ctx.ui.confirm(
|
|
581
|
-
formatTelegramTakeoverTitle(ctx),
|
|
582
|
-
formatTelegramTakeoverPrompt(ctx, result.owner),
|
|
583
|
-
);
|
|
584
|
-
if (!confirmed) {
|
|
585
|
-
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
586
|
-
deps.updateStatus(ctx);
|
|
587
|
-
return;
|
|
619
|
+
if (result && !result.ok) {
|
|
620
|
+
if (result.message) deps.recordConnectionEvent?.(result.message, "connect-refused");
|
|
621
|
+
ctx.ui.notify(result.notice ?? formatTelegramConnectionFailure(result.message), "warning");
|
|
622
|
+
} else if (result?.message) {
|
|
623
|
+
ctx.ui.notify(result.message, "info");
|
|
588
624
|
}
|
|
589
|
-
result
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
deps.queueAgentConnectionContext?.(true);
|
|
625
|
+
if (!result || result.ok) deps.queueAgentConnectionContext?.(true);
|
|
626
|
+
deps.updateStatus(ctx);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (!isCurrent()) return;
|
|
629
|
+
deps.recordConnectionEvent?.(error, "connect");
|
|
630
|
+
ctx.ui.notify(formatTelegramConnectionFailure(error), "warning");
|
|
631
|
+
deps.updateStatus(ctx);
|
|
632
|
+
} finally {
|
|
633
|
+
if (intentId) deps.connectionIntent?.finish(intentId);
|
|
599
634
|
}
|
|
600
|
-
deps.updateStatus(ctx);
|
|
601
635
|
},
|
|
602
636
|
});
|
|
603
637
|
pi.registerCommand("telegram-disconnect", {
|
|
604
638
|
description: "Stop Telegram and delete current thread in Threaded Mode",
|
|
605
639
|
handler: async (_args, ctx) => {
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (!confirmed) {
|
|
613
|
-
ctx.ui.notify("Telegram disconnect cancelled.", "info");
|
|
614
|
-
deps.updateStatus(ctx);
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
640
|
+
const generation = deps.getSessionGeneration?.();
|
|
641
|
+
const isCurrent = () =>
|
|
642
|
+
(generation === undefined || deps.getSessionGeneration?.() === generation) &&
|
|
643
|
+
deps.isContextCurrent?.(ctx) !== false;
|
|
644
|
+
if (!isCurrent()) return;
|
|
645
|
+
deps.connectionIntent?.cancel();
|
|
618
646
|
try {
|
|
647
|
+
const threadName = deps.getDisconnectThreadName?.();
|
|
648
|
+
if (threadName) {
|
|
649
|
+
const confirmed = await ctx.ui.confirm(
|
|
650
|
+
ctx.ui.theme.fg("accent", "pi-telegram"),
|
|
651
|
+
`Delete Telegram thread ${ctx.ui.theme.fg("warning", threadName)} and disconnect this Pi session?`,
|
|
652
|
+
);
|
|
653
|
+
if (!isCurrent()) return;
|
|
654
|
+
if (!confirmed) {
|
|
655
|
+
ctx.ui.notify("Telegram disconnect cancelled.", "info");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
619
659
|
const message = await deps.stopPolling();
|
|
660
|
+
if (!isCurrent()) return;
|
|
620
661
|
if (message) ctx.ui.notify(message, "info");
|
|
621
662
|
deps.queueAgentConnectionContext?.(false);
|
|
622
663
|
} catch (error) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
"warning",
|
|
627
|
-
);
|
|
628
|
-
throw error;
|
|
664
|
+
deps.recordConnectionEvent?.(error, "disconnect");
|
|
665
|
+
if (!isCurrent()) return;
|
|
666
|
+
ctx.ui.notify("Telegram disconnect incomplete; keep Pi open. Check /telegram-status --debug.", "warning");
|
|
629
667
|
} finally {
|
|
630
|
-
deps.updateStatus(ctx);
|
|
668
|
+
if (isCurrent()) deps.updateStatus(ctx);
|
|
631
669
|
}
|
|
632
670
|
},
|
|
633
671
|
});
|
|
@@ -1633,6 +1633,27 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1633
1633
|
resolveAutomaticThreadCleanupEnabled: configControls.resolveAutomaticThreadCleanupEnabled,
|
|
1634
1634
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1635
1635
|
});
|
|
1636
|
+
const connectionIntent = Lifecycle.createTelegramConnectionIntentRuntime();
|
|
1637
|
+
const connectionLifecycle = Lifecycle.createTelegramConnectionLifecycle({
|
|
1638
|
+
intent: connectionIntent,
|
|
1639
|
+
getGeneration: telegramSessionContextStore.getGeneration,
|
|
1640
|
+
isCurrent: telegramSessionContextStore.isCurrent,
|
|
1641
|
+
getProfileName: configStore.getActiveProfileName,
|
|
1642
|
+
isConnected() {
|
|
1643
|
+
return lockRuntime.owns() || telegramBusFollowerRegistrationState.isRegistered();
|
|
1644
|
+
},
|
|
1645
|
+
async activateProfile(profileName, isCurrent) {
|
|
1646
|
+
await configStore.load();
|
|
1647
|
+
if (!isCurrent()) return false;
|
|
1648
|
+
return configStore.activateProfile(profileName) && configStore.hasBotToken();
|
|
1649
|
+
},
|
|
1650
|
+
start(ctx) {
|
|
1651
|
+
return lockedPollingRuntime.start(ctx);
|
|
1652
|
+
},
|
|
1653
|
+
recordError(error) {
|
|
1654
|
+
recordRuntimeEvent("connection", error, { phase: "resume-connect" });
|
|
1655
|
+
},
|
|
1656
|
+
});
|
|
1636
1657
|
const telegramBridgeSessionLifecycleDeps =
|
|
1637
1658
|
Lifecycle.createTelegramBridgeSessionLifecycleDeps({
|
|
1638
1659
|
contextStore: telegramSessionContextStore,
|
|
@@ -1681,6 +1702,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1681
1702
|
},
|
|
1682
1703
|
delivery: deliveryLifecycleRuntime,
|
|
1683
1704
|
polling: lockedPollingRuntime,
|
|
1705
|
+
connection: connectionLifecycle,
|
|
1684
1706
|
inboundWorker: {
|
|
1685
1707
|
onSessionShutdown: updateAdmissionRuntimeBinding.onSessionShutdown,
|
|
1686
1708
|
},
|
|
@@ -1827,6 +1849,9 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1827
1849
|
modelContextAvailabilityRuntime.reconcile();
|
|
1828
1850
|
},
|
|
1829
1851
|
getStatusLines,
|
|
1852
|
+
isContextCurrent: telegramSessionContextStore.isCurrent,
|
|
1853
|
+
getSessionGeneration: telegramSessionContextStore.getGeneration,
|
|
1854
|
+
connectionIntent,
|
|
1830
1855
|
buttonActionStore,
|
|
1831
1856
|
sendMarkdownReply,
|
|
1832
1857
|
async sendChannelMarkdownMessage(channel, markdown, options) {
|
|
@@ -1,9 +1,12 @@
|
|
|
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
|
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { formatTelegramConnectionFailure } from "./status.ts";
|
|
7
10
|
import * as BusFollower from "./bus-follower.ts";
|
|
8
11
|
import * as Queue from "./queue.ts";
|
|
9
12
|
import * as TextGroups from "./text-groups.ts";
|
|
@@ -157,6 +160,130 @@ export interface TelegramSessionLifecycleHooks {
|
|
|
157
160
|
) => Promise<void>;
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
export interface TelegramConnectionIntent {
|
|
164
|
+
id: string;
|
|
165
|
+
cwd: string;
|
|
166
|
+
profileName?: string;
|
|
167
|
+
}
|
|
168
|
+
interface TelegramConnectionHandoff extends TelegramConnectionIntent {
|
|
169
|
+
pid: number;
|
|
170
|
+
targetSessionFile: string;
|
|
171
|
+
expiresAtMs: number;
|
|
172
|
+
}
|
|
173
|
+
export interface TelegramConnectionHandoffStore {
|
|
174
|
+
pending?: TelegramConnectionHandoff;
|
|
175
|
+
}
|
|
176
|
+
const HANDOFF_KEY = Symbol.for("pi-telegram.connection-resume.v1");
|
|
177
|
+
function processHandoffStore(): TelegramConnectionHandoffStore {
|
|
178
|
+
const globals = globalThis as unknown as Record<symbol, TelegramConnectionHandoffStore>;
|
|
179
|
+
return globals[HANDOFF_KEY] ??= {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface TelegramConnectionLifecycle {
|
|
183
|
+
prepare(event: SessionStartEvent, ctx: ExtensionContext): (() => void) | undefined;
|
|
184
|
+
onSessionShutdown(event: SessionShutdownEvent, ctx: ExtensionContext): void;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function createTelegramConnectionLifecycle(deps: {
|
|
188
|
+
intent: ReturnType<typeof createTelegramConnectionIntentRuntime>;
|
|
189
|
+
getGeneration(): number;
|
|
190
|
+
isCurrent(ctx: ExtensionContext): boolean;
|
|
191
|
+
getProfileName(): string | undefined;
|
|
192
|
+
isConnected(): boolean;
|
|
193
|
+
activateProfile(profileName: string | undefined, isCurrent: () => boolean): Promise<boolean>;
|
|
194
|
+
start(ctx: ExtensionContext): Promise<{ ok: boolean; message?: string }>;
|
|
195
|
+
recordError(error: unknown): void;
|
|
196
|
+
}): TelegramConnectionLifecycle {
|
|
197
|
+
return {
|
|
198
|
+
onSessionShutdown(event, ctx) {
|
|
199
|
+
deps.intent.suspend({ reason: event.reason, cwd: ctx.cwd,
|
|
200
|
+
targetSessionFile: event.targetSessionFile, connected: deps.isConnected(),
|
|
201
|
+
profileName: deps.getProfileName() });
|
|
202
|
+
},
|
|
203
|
+
prepare(event, ctx) {
|
|
204
|
+
const intent = deps.intent.resume({ reason: event.reason, cwd: ctx.cwd,
|
|
205
|
+
sessionFile: event.reason === "resume" ? ctx.sessionManager.getSessionFile() : undefined });
|
|
206
|
+
if (!intent) return undefined;
|
|
207
|
+
const generation = deps.getGeneration();
|
|
208
|
+
const isCurrent = () => deps.intent.isActive(intent.id) &&
|
|
209
|
+
generation === deps.getGeneration() && deps.isCurrent(ctx);
|
|
210
|
+
return () => {
|
|
211
|
+
// Startup remains extension-owned background work; no captured command ctx crosses resume.
|
|
212
|
+
void (async () => {
|
|
213
|
+
if (!isCurrent()) return;
|
|
214
|
+
if (!await deps.activateProfile(intent.profileName, isCurrent)) {
|
|
215
|
+
if (isCurrent()) ctx.ui.notify("Telegram profile unavailable. Run /telegram-setup.", "warning");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!isCurrent()) return;
|
|
219
|
+
const result = await deps.start(ctx);
|
|
220
|
+
if (!isCurrent()) return;
|
|
221
|
+
if (!result.ok) {
|
|
222
|
+
deps.recordError(new Error(result.message ?? "Telegram resume connection failed."));
|
|
223
|
+
ctx.ui.notify(formatTelegramConnectionFailure(result.message), "warning");
|
|
224
|
+
}
|
|
225
|
+
})().catch((error) => {
|
|
226
|
+
deps.recordError(error);
|
|
227
|
+
if (isCurrent()) ctx.ui.notify(formatTelegramConnectionFailure(error), "warning");
|
|
228
|
+
}).finally(() => deps.intent.finish(intent.id));
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function createTelegramConnectionIntentRuntime(options: {
|
|
235
|
+
store?: TelegramConnectionHandoffStore;
|
|
236
|
+
now?: () => number;
|
|
237
|
+
pid?: number;
|
|
238
|
+
} = {}) {
|
|
239
|
+
const store = options.store ?? processHandoffStore();
|
|
240
|
+
const now = options.now ?? Date.now;
|
|
241
|
+
const pid = options.pid ?? process.pid;
|
|
242
|
+
let active: TelegramConnectionIntent | undefined;
|
|
243
|
+
return {
|
|
244
|
+
begin(cwd: string, profileName?: string): string {
|
|
245
|
+
store.pending = undefined;
|
|
246
|
+
active = { id: randomUUID(), cwd, profileName };
|
|
247
|
+
return active.id;
|
|
248
|
+
},
|
|
249
|
+
isActive(id: string): boolean {
|
|
250
|
+
return active?.id === id;
|
|
251
|
+
},
|
|
252
|
+
finish(id: string): void {
|
|
253
|
+
if (active?.id === id) active = undefined;
|
|
254
|
+
},
|
|
255
|
+
cancel(): void {
|
|
256
|
+
active = undefined;
|
|
257
|
+
store.pending = undefined;
|
|
258
|
+
},
|
|
259
|
+
suspend(input: {
|
|
260
|
+
reason: string;
|
|
261
|
+
cwd: string;
|
|
262
|
+
targetSessionFile?: string;
|
|
263
|
+
connected: boolean;
|
|
264
|
+
profileName?: string;
|
|
265
|
+
}): void {
|
|
266
|
+
const intent = active ?? (input.connected
|
|
267
|
+
? { id: randomUUID(), cwd: input.cwd, profileName: input.profileName }
|
|
268
|
+
: undefined);
|
|
269
|
+
active = undefined;
|
|
270
|
+
store.pending = input.reason === "resume" && input.targetSessionFile &&
|
|
271
|
+
intent?.cwd === input.cwd
|
|
272
|
+
? { ...intent, pid, targetSessionFile: input.targetSessionFile, expiresAtMs: now() + 30_000 }
|
|
273
|
+
: undefined;
|
|
274
|
+
},
|
|
275
|
+
resume(input: { reason: string; cwd: string; sessionFile?: string }): TelegramConnectionIntent | undefined {
|
|
276
|
+
const handoff = store.pending;
|
|
277
|
+
store.pending = undefined;
|
|
278
|
+
if (!handoff || handoff.pid !== pid || now() >= handoff.expiresAtMs ||
|
|
279
|
+
input.reason !== "resume" || input.cwd !== handoff.cwd ||
|
|
280
|
+
input.sessionFile !== handoff.targetSessionFile) return undefined;
|
|
281
|
+
active = { id: randomUUID(), cwd: handoff.cwd, profileName: handoff.profileName };
|
|
282
|
+
return { ...active };
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
160
287
|
export interface TelegramSessionContextStore<TContext> {
|
|
161
288
|
get: () => TContext | undefined;
|
|
162
289
|
getGeneration: () => number;
|
|
@@ -250,6 +377,7 @@ export function createTelegramSessionGenerationFence(
|
|
|
250
377
|
}
|
|
251
378
|
|
|
252
379
|
export interface TelegramBridgeSessionServiceRuntime {
|
|
380
|
+
connection?: TelegramConnectionLifecycle;
|
|
253
381
|
resumeGroupedInput(ctx: ExtensionContext): void;
|
|
254
382
|
suspendGroupedInput(): void;
|
|
255
383
|
delivery: {
|
|
@@ -319,6 +447,7 @@ export interface TelegramBridgeSessionLifecyclePorts<
|
|
|
319
447
|
};
|
|
320
448
|
delivery: TelegramBridgeSessionServiceRuntime["delivery"];
|
|
321
449
|
polling: TelegramBridgeSessionServiceRuntime["polling"];
|
|
450
|
+
connection?: TelegramBridgeSessionServiceRuntime["connection"];
|
|
322
451
|
inboundWorker: TelegramBridgeSessionServiceRuntime["inboundWorker"];
|
|
323
452
|
capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
|
|
324
453
|
queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
|
|
@@ -348,6 +477,7 @@ export function createTelegramBridgeSessionLifecycleDeps<
|
|
|
348
477
|
}),
|
|
349
478
|
delivery: ports.services.delivery,
|
|
350
479
|
polling: ports.services.polling,
|
|
480
|
+
connection: ports.services.connection,
|
|
351
481
|
inboundWorker: ports.services.inboundWorker,
|
|
352
482
|
capabilityMonitor: ports.services.capabilityMonitor,
|
|
353
483
|
queueWatchdog: ports.services.queueWatchdog,
|
|
@@ -371,19 +501,23 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
371
501
|
suspendPolling: deps.follower.suspendPolling,
|
|
372
502
|
recordRuntimeEvent: deps.follower.recordRuntimeEvent,
|
|
373
503
|
});
|
|
504
|
+
let preserveTarget = true;
|
|
374
505
|
const queueLifecycle = Queue.createTelegramSessionLifecycleRuntime({
|
|
375
506
|
...deps.queue,
|
|
376
507
|
isSessionActive,
|
|
377
|
-
stopPolling: suspendForReplacement,
|
|
508
|
+
stopPolling: () => suspendForReplacement(preserveTarget),
|
|
378
509
|
clearPendingMediaGroups: deps.services.suspendGroupedInput,
|
|
379
510
|
});
|
|
380
511
|
const servicesLifecycle: TelegramSessionLifecycleHooks = {
|
|
381
512
|
async onSessionStart(event, ctx) {
|
|
513
|
+
const resumeConnection = deps.services.connection?.prepare(event, ctx);
|
|
382
514
|
await queueLifecycle.onSessionStart(event, ctx);
|
|
383
515
|
if (!isSessionActive(ctx)) return;
|
|
384
516
|
deps.services.resumeGroupedInput(ctx);
|
|
385
517
|
await deps.services.delivery.onSessionStart();
|
|
386
|
-
|
|
518
|
+
if (!isSessionActive(ctx)) return;
|
|
519
|
+
if (resumeConnection) resumeConnection();
|
|
520
|
+
else await deps.services.polling.onSessionStart(event, ctx);
|
|
387
521
|
deps.services.capabilityMonitor.start(ctx);
|
|
388
522
|
deps.services.queueWatchdog.start(ctx);
|
|
389
523
|
},
|
|
@@ -391,6 +525,8 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
391
525
|
const generation = deps.contextStore.getGeneration();
|
|
392
526
|
const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
|
|
393
527
|
if (!isCurrent()) return;
|
|
528
|
+
deps.services.connection?.onSessionShutdown(event, ctx);
|
|
529
|
+
preserveTarget = event.reason !== "resume";
|
|
394
530
|
let preserveThread: (() => Promise<void>) | undefined;
|
|
395
531
|
if (event.reason === "quit") {
|
|
396
532
|
try {
|
|
@@ -454,10 +454,8 @@ export function buildTelegramModelSwitchContinuationText<
|
|
|
454
454
|
thinkingLevel?: ScopedTelegramModel<TModel>["thinkingLevel"],
|
|
455
455
|
): string {
|
|
456
456
|
const modelLabel = `${model.provider}/${model.id}`;
|
|
457
|
-
const thinkingSuffix = thinkingLevel
|
|
458
|
-
|
|
459
|
-
: "";
|
|
460
|
-
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}`;
|
|
457
|
+
const thinkingSuffix = thinkingLevel ? `; thinking: ${thinkingLevel}` : "";
|
|
458
|
+
return `${telegramPrefix} Continue from the last unfinished step. Model: ${modelLabel}${thinkingSuffix}.`;
|
|
461
459
|
}
|
|
462
460
|
|
|
463
461
|
export type TelegramModelSwitchContinuationSource = Pick<
|
|
@@ -1,9 +1,38 @@
|
|
|
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
6
|
|
|
7
|
+
/** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
|
|
8
|
+
export function formatTelegramConnectionFailure(error: unknown): string {
|
|
9
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
10
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : undefined;
|
|
11
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
12
|
+
if (status === 401) return "Telegram token rejected. Run /telegram-setup.";
|
|
13
|
+
if (status === 403) return "Telegram access denied. Check /telegram-status --debug.";
|
|
14
|
+
if ((typeof code === "string" && ["ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "ECONNRESET"].includes(code)) ||
|
|
15
|
+
/network unavailable|fetch failed/i.test(message)) {
|
|
16
|
+
return "Telegram network unavailable. Retry /telegram-connect.";
|
|
17
|
+
}
|
|
18
|
+
if (/Workspace slots?.*(?:unavailable|exhausted)|no free.*slot/i.test(message)) {
|
|
19
|
+
return "No Telegram slot available. Check /telegram-status --debug.";
|
|
20
|
+
}
|
|
21
|
+
if (code === "incompatible-protocol" || /protocol.*(?:incompatible|mismatch)|incompatible.*protocol/i.test(message)) {
|
|
22
|
+
return "Telegram instances are incompatible. Update them together.";
|
|
23
|
+
}
|
|
24
|
+
if (/unsupported.*(?:journal|version)|(?:journal|version).*unsupported/i.test(message)) {
|
|
25
|
+
return "Telegram state version unsupported. Use a compatible runtime.";
|
|
26
|
+
}
|
|
27
|
+
if (/follower registration failed|active in another Pi instance/i.test(message)) {
|
|
28
|
+
return "Telegram leader is active but unavailable. Check /telegram-status --debug.";
|
|
29
|
+
}
|
|
30
|
+
if (/unfinished Thread creation does not match/i.test(message)) {
|
|
31
|
+
return "Telegram Thread creation is unresolved. Check /telegram-status --debug.";
|
|
32
|
+
}
|
|
33
|
+
return "Telegram connection failed. Check /telegram-status --debug.";
|
|
34
|
+
}
|
|
35
|
+
|
|
7
36
|
const TELEGRAM_STATUS_DEFAULT_PROFILE_NAME = "default";
|
|
8
37
|
|
|
9
38
|
export type TelegramStatusQueueLane = "control" | "priority" | "default";
|
|
@@ -773,6 +773,7 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
773
773
|
: undefined;
|
|
774
774
|
const legacyWorkspaceBinding =
|
|
775
775
|
workspaceIdentity?.instanceSlot === "a" &&
|
|
776
|
+
deps.sessionId === undefined &&
|
|
776
777
|
!persistedWorkspaceBinding &&
|
|
777
778
|
typeof legacyLeaderRecord?.target.threadId === "number"
|
|
778
779
|
? {
|
|
@@ -885,10 +886,9 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
885
886
|
(record.status === "active" || record.status === "starting")
|
|
886
887
|
);
|
|
887
888
|
});
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
if (!deps.forceFreshUnnamed && priorTargets.length > 0) {
|
|
889
|
+
// Legacy callers without session identity may reuse the active instance target.
|
|
890
|
+
// Session-aware callers must resolve their exact Workspace binding instead.
|
|
891
|
+
if (deps.sessionId === undefined && !deps.forceFreshUnnamed && priorTargets.length > 0) {
|
|
892
892
|
const record = priorTargets[0];
|
|
893
893
|
deps.recordEvent(
|
|
894
894
|
"telegram",
|