@llblab/pi-kit 0.14.1 → 0.16.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/CHANGELOG.md +11 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +6 -0
- package/node_modules/@llblab/pi-codex-usage/index.ts +81 -7
- package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +9 -9
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +0 -13
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +15 -0
- package/node_modules/@llblab/pi-state-flow/README.md +7 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +8 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +93 -35
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +34 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +14 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +182 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +5 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +8 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +7 -3
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +11 -3
- package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -3
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +504 -0
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +11 -8
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -4
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +86 -34
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +33 -16
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -16
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +173 -9
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +11 -6
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +19 -6
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +7 -3
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +11 -3
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +12 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +7 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +59 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
- package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +14 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +10 -2
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +7 -1
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +90 -19
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +4 -4
|
@@ -645,6 +645,50 @@ function normalizeWorkspaceBindingRecord(value) {
|
|
|
645
645
|
function cloneWorkspaceBinding(binding) {
|
|
646
646
|
return { ...binding, target: { ...binding.target } };
|
|
647
647
|
}
|
|
648
|
+
function cloneSessionReplacementIntent(intent) {
|
|
649
|
+
return { ...intent, target: { ...intent.target } };
|
|
650
|
+
}
|
|
651
|
+
function normalizeSessionReplacementIntent(value) {
|
|
652
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
653
|
+
return undefined;
|
|
654
|
+
const record = value;
|
|
655
|
+
const target = record.target;
|
|
656
|
+
if (typeof record.cwd !== "string" || !normalizeTelegramWorkspacePath(record.cwd) ||
|
|
657
|
+
typeof record.profileName !== "string" || !record.profileName ||
|
|
658
|
+
typeof record.sourceSessionId !== "string" || !normalizeTelegramSessionId(record.sourceSessionId) ||
|
|
659
|
+
typeof record.sourceUpdateId !== "number" || !Number.isSafeInteger(record.sourceUpdateId) ||
|
|
660
|
+
!target || typeof target.chatId !== "number" ||
|
|
661
|
+
(target.threadId !== undefined &&
|
|
662
|
+
(typeof target.threadId !== "number" || !Number.isSafeInteger(target.threadId))) ||
|
|
663
|
+
typeof record.messageId !== "number" || !Number.isSafeInteger(record.messageId) ||
|
|
664
|
+
typeof record.createdAtMs !== "number" || !Number.isSafeInteger(record.createdAtMs) ||
|
|
665
|
+
typeof record.expiresAtMs !== "number" || !Number.isSafeInteger(record.expiresAtMs) ||
|
|
666
|
+
record.expiresAtMs <= record.createdAtMs)
|
|
667
|
+
return undefined;
|
|
668
|
+
const continuity = record.continuity === "workspace-thread" ||
|
|
669
|
+
record.continuity === "classic-chat"
|
|
670
|
+
? record.continuity
|
|
671
|
+
: target.threadId !== undefined ? "workspace-thread" : "classic-chat";
|
|
672
|
+
if ((continuity === "workspace-thread") !== (target.threadId !== undefined)) {
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
continuity,
|
|
677
|
+
cwd: normalizeTelegramWorkspacePath(record.cwd),
|
|
678
|
+
profileName: record.profileName,
|
|
679
|
+
sourceSessionId: normalizeTelegramSessionId(record.sourceSessionId),
|
|
680
|
+
sourceUpdateId: record.sourceUpdateId,
|
|
681
|
+
target: {
|
|
682
|
+
chatId: target.chatId,
|
|
683
|
+
...(typeof target.threadId === "number" ? { threadId: target.threadId } : {}),
|
|
684
|
+
},
|
|
685
|
+
messageId: record.messageId,
|
|
686
|
+
...(typeof record.slot === "string" ? { slot: record.slot } : {}),
|
|
687
|
+
...(typeof record.threadName === "string" ? { threadName: record.threadName } : {}),
|
|
688
|
+
createdAtMs: record.createdAtMs,
|
|
689
|
+
expiresAtMs: record.expiresAtMs,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
648
692
|
function normalizeWorkspaceRetirementIntent(value) {
|
|
649
693
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
650
694
|
return undefined;
|
|
@@ -905,6 +949,7 @@ function parseTopicTargetFile(value) {
|
|
|
905
949
|
return normalized ? [normalized] : [];
|
|
906
950
|
})
|
|
907
951
|
: [],
|
|
952
|
+
sessionReplacement: normalizeSessionReplacementIntent(file.sessionReplacement),
|
|
908
953
|
reservations: Array.isArray(file.reservations)
|
|
909
954
|
? file.reservations.flatMap((reservation) => {
|
|
910
955
|
const normalized = normalizeReservation(reservation);
|
|
@@ -1026,6 +1071,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1026
1071
|
let identities = new Map();
|
|
1027
1072
|
let workspaceBindings = new Map();
|
|
1028
1073
|
let workspaceRetirements = [];
|
|
1074
|
+
let sessionReplacement;
|
|
1029
1075
|
let workspaceRetirementCommitInFlight = false;
|
|
1030
1076
|
const hasWorkspaceRetirementConflict = (input) => workspaceRetirements.some((intent) => (input.bindingKey !== undefined && intent.binding.bindingKey === input.bindingKey) ||
|
|
1031
1077
|
(input.cwd !== undefined && intent.binding.cwd === input.cwd) ||
|
|
@@ -1150,6 +1196,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1150
1196
|
identities = new Map();
|
|
1151
1197
|
workspaceBindings = new Map();
|
|
1152
1198
|
workspaceRetirements = [];
|
|
1199
|
+
sessionReplacement = undefined;
|
|
1153
1200
|
workspaceRetirementCommitInFlight = false;
|
|
1154
1201
|
workspaceClaims = new Map();
|
|
1155
1202
|
reservations = [];
|
|
@@ -1171,6 +1218,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1171
1218
|
identities = new Map();
|
|
1172
1219
|
workspaceBindings = new Map();
|
|
1173
1220
|
workspaceRetirements = [];
|
|
1221
|
+
sessionReplacement = undefined;
|
|
1174
1222
|
workspaceRetirementCommitInFlight = false;
|
|
1175
1223
|
reservations = [];
|
|
1176
1224
|
pendingProvisions = [];
|
|
@@ -1208,6 +1256,9 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1208
1256
|
cloneWorkspaceBinding(binding),
|
|
1209
1257
|
]));
|
|
1210
1258
|
workspaceRetirements = (file.workspaceRetirements ?? []).map(cloneWorkspaceRetirementIntent);
|
|
1259
|
+
sessionReplacement = file.sessionReplacement
|
|
1260
|
+
? cloneSessionReplacementIntent(file.sessionReplacement)
|
|
1261
|
+
: undefined;
|
|
1211
1262
|
reconcileWorkspaceSuffixExposure();
|
|
1212
1263
|
for (const record of records.values())
|
|
1213
1264
|
rememberIdentity(record);
|
|
@@ -1286,6 +1337,9 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1286
1337
|
identities: Array.from(identities.values()).map(cloneIdentityRecord),
|
|
1287
1338
|
workspaceBindings: Array.from(workspaceBindings.values()).map(cloneWorkspaceBinding),
|
|
1288
1339
|
workspaceRetirements: workspaceRetirements.map(cloneWorkspaceRetirementIntent),
|
|
1340
|
+
...(sessionReplacement
|
|
1341
|
+
? { sessionReplacement: cloneSessionReplacementIntent(sessionReplacement) }
|
|
1342
|
+
: {}),
|
|
1289
1343
|
reservations: reservations.map((reservation) => ({ ...reservation })),
|
|
1290
1344
|
pendingProvisions: pendingProvisions.map((provision) => ({
|
|
1291
1345
|
...provision,
|
|
@@ -1563,6 +1617,40 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1563
1617
|
listWorkspaceBindings() {
|
|
1564
1618
|
return Array.from(workspaceBindings.values()).map(cloneWorkspaceBinding);
|
|
1565
1619
|
},
|
|
1620
|
+
getWorkspaceBindingByTarget(target, sessionId) {
|
|
1621
|
+
const binding = Array.from(workspaceBindings.values()).find((candidate) => targetMatches(candidate.target, target) &&
|
|
1622
|
+
(sessionId === undefined || candidate.sessionId === sessionId));
|
|
1623
|
+
return binding ? cloneWorkspaceBinding(binding) : undefined;
|
|
1624
|
+
},
|
|
1625
|
+
getSessionReplacementIntent() {
|
|
1626
|
+
return sessionReplacement
|
|
1627
|
+
? cloneSessionReplacementIntent(sessionReplacement)
|
|
1628
|
+
: undefined;
|
|
1629
|
+
},
|
|
1630
|
+
async commitSessionReplacementIntent(intent, isCurrent) {
|
|
1631
|
+
const next = normalizeSessionReplacementIntent(intent);
|
|
1632
|
+
if (!next || !isCurrent())
|
|
1633
|
+
return false;
|
|
1634
|
+
await loadFromDisk();
|
|
1635
|
+
if (!isCurrent())
|
|
1636
|
+
return false;
|
|
1637
|
+
const existing = sessionReplacement;
|
|
1638
|
+
if (existing && existing.expiresAtMs > getNowMs() &&
|
|
1639
|
+
!isDeepStrictEqual(existing, next))
|
|
1640
|
+
return false;
|
|
1641
|
+
sessionReplacement = cloneSessionReplacementIntent(next);
|
|
1642
|
+
markDirty();
|
|
1643
|
+
return await persistSnapshot() && isCurrent();
|
|
1644
|
+
},
|
|
1645
|
+
async removeSessionReplacementIntent(expected, isCurrent) {
|
|
1646
|
+
await loadFromDisk();
|
|
1647
|
+
if (!isCurrent() || !sessionReplacement ||
|
|
1648
|
+
!isDeepStrictEqual(sessionReplacement, expected))
|
|
1649
|
+
return false;
|
|
1650
|
+
sessionReplacement = undefined;
|
|
1651
|
+
markDirty();
|
|
1652
|
+
return await persistSnapshot() && isCurrent();
|
|
1653
|
+
},
|
|
1566
1654
|
listWorkspaceRetirementIntents() {
|
|
1567
1655
|
return workspaceRetirements.map(cloneWorkspaceRetirementIntent);
|
|
1568
1656
|
},
|
|
@@ -1959,6 +2047,36 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1959
2047
|
(options?.sessionId !== undefined && !normalizedSessionId) ||
|
|
1960
2048
|
hasWorkspaceRetirementConflict({ cwd: normalizedCwd }))
|
|
1961
2049
|
return undefined;
|
|
2050
|
+
let replacementPreviousInstanceId;
|
|
2051
|
+
const replacement = sessionReplacement;
|
|
2052
|
+
if (replacement?.continuity === "workspace-thread" && normalizedSessionId &&
|
|
2053
|
+
replacement.expiresAtMs > getNowMs() &&
|
|
2054
|
+
replacement.profileName === (getTelegramProfile() ?? "default") &&
|
|
2055
|
+
replacement.cwd === normalizedCwd &&
|
|
2056
|
+
replacement.sourceSessionId !== normalizedSessionId) {
|
|
2057
|
+
const sourceEntry = Array.from(workspaceBindings.entries()).find(([, binding]) => binding.cwd === normalizedCwd &&
|
|
2058
|
+
binding.sessionId === replacement.sourceSessionId &&
|
|
2059
|
+
targetMatches(binding.target, replacement.target));
|
|
2060
|
+
if (sourceEntry) {
|
|
2061
|
+
const [sourceKey, sourceBinding] = sourceEntry;
|
|
2062
|
+
const replacementIdentity = Array.from({ length: TELEGRAM_WORKSPACE_SLOTS.length })
|
|
2063
|
+
.map((_, ordinal) => createTelegramWorkspaceBindingIdentityWithKey(sourceBinding.cwd, sourceBinding.workspaceKey, ordinal, normalizedSessionId))
|
|
2064
|
+
.find((identity) => identity?.instanceSlot === sourceBinding.instanceSlot);
|
|
2065
|
+
if (replacementIdentity &&
|
|
2066
|
+
replacementIdentity.instanceSlot === sourceBinding.instanceSlot) {
|
|
2067
|
+
const existingTargetRecord = Array.from(records.values()).find((record) => targetMatches(record.target, replacement.target));
|
|
2068
|
+
replacementPreviousInstanceId = existingTargetRecord?.instanceId;
|
|
2069
|
+
workspaceBindings.delete(sourceKey);
|
|
2070
|
+
workspaceBindings.set(getWorkspaceBindingMapKey(replacementIdentity), {
|
|
2071
|
+
...sourceBinding,
|
|
2072
|
+
...replacementIdentity,
|
|
2073
|
+
updatedAtMs: getNowMs(),
|
|
2074
|
+
});
|
|
2075
|
+
markDirty();
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
const effectivePreviousInstanceId = previousInstanceId ?? replacementPreviousInstanceId;
|
|
1962
2080
|
const externalReservedSlots = captureExternalReservedSlots();
|
|
1963
2081
|
if (!externalReservedSlots) {
|
|
1964
2082
|
options?.onCapacityUnavailable?.();
|
|
@@ -1995,9 +2113,16 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1995
2113
|
const mapKey = getWorkspaceBindingMapKey(identity);
|
|
1996
2114
|
const existingClaim = workspaceClaims.get(mapKey);
|
|
1997
2115
|
if (existingClaim) {
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2116
|
+
if (existingClaim.instanceId === instanceId) {
|
|
2117
|
+
return { ...existingClaim.identity };
|
|
2118
|
+
}
|
|
2119
|
+
if (existingClaim.instanceId !== effectivePreviousInstanceId)
|
|
2120
|
+
return undefined;
|
|
2121
|
+
workspaceClaims.set(mapKey, {
|
|
2122
|
+
identity: existingClaim.identity,
|
|
2123
|
+
instanceId,
|
|
2124
|
+
});
|
|
2125
|
+
return { ...existingClaim.identity };
|
|
2001
2126
|
}
|
|
2002
2127
|
const binding = workspaceBindings.get(mapKey);
|
|
2003
2128
|
const liveRecord = binding
|
|
@@ -2005,7 +2130,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2005
2130
|
: undefined;
|
|
2006
2131
|
if (liveRecord &&
|
|
2007
2132
|
liveRecord.instanceId !== instanceId &&
|
|
2008
|
-
liveRecord.instanceId !==
|
|
2133
|
+
liveRecord.instanceId !== effectivePreviousInstanceId) {
|
|
2009
2134
|
return undefined;
|
|
2010
2135
|
}
|
|
2011
2136
|
const retainedTarget = binding?.target ?? legacyRecord?.target;
|
|
@@ -2147,8 +2272,29 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2147
2272
|
if (!next)
|
|
2148
2273
|
return undefined;
|
|
2149
2274
|
const nextMapKey = getWorkspaceBindingMapKey(next);
|
|
2150
|
-
|
|
2151
|
-
|
|
2275
|
+
let claim = workspaceClaims.get(nextMapKey);
|
|
2276
|
+
const replacedTargetBinding = Array.from(workspaceBindings.values()).find((existing) => existing.bindingKey !== next.bindingKey &&
|
|
2277
|
+
targetMatches(existing.target, next.target));
|
|
2278
|
+
if (claimInstanceId && claim?.instanceId === claimInstanceId &&
|
|
2279
|
+
replacedTargetBinding?.slot) {
|
|
2280
|
+
claim = {
|
|
2281
|
+
...claim,
|
|
2282
|
+
identity: { ...claim.identity, slot: replacedTargetBinding.slot },
|
|
2283
|
+
};
|
|
2284
|
+
workspaceClaims.set(nextMapKey, claim);
|
|
2285
|
+
next.slot = replacedTargetBinding.slot;
|
|
2286
|
+
if (replacedTargetBinding.threadName && !next.threadName) {
|
|
2287
|
+
next.threadName = replacedTargetBinding.threadName;
|
|
2288
|
+
}
|
|
2289
|
+
if (replacedTargetBinding.manualThreadName && !next.manualThreadName) {
|
|
2290
|
+
next.manualThreadName = replacedTargetBinding.manualThreadName;
|
|
2291
|
+
}
|
|
2292
|
+
if (replacedTargetBinding.displayTitle && !next.displayTitle) {
|
|
2293
|
+
next.displayTitle = replacedTargetBinding.displayTitle;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
if (next.slot && Array.from(workspaceBindings.values()).some((existing) => existing.bindingKey !== next.bindingKey && existing.slot === next.slot &&
|
|
2297
|
+
!targetMatches(existing.target, next.target)))
|
|
2152
2298
|
return undefined;
|
|
2153
2299
|
for (const existing of workspaceBindings.values()) {
|
|
2154
2300
|
if (existing.workspaceKey === next.workspaceKey &&
|
|
@@ -1216,6 +1216,7 @@ export interface TelegramUpdateWorkerOwnerRuntimeDeps<TContext> {
|
|
|
1216
1216
|
isContextCurrent: (ctx: TContext) => boolean;
|
|
1217
1217
|
dispatchNext: (ctx: TContext) => void;
|
|
1218
1218
|
requestQueueHandoffReconciliation: (ctx: TContext) => void;
|
|
1219
|
+
afterUpdateCompleted?: (updateId: number) => void;
|
|
1219
1220
|
}
|
|
1220
1221
|
export declare function createTelegramUpdateWorkerOwnerRuntime<TContext>(deps: TelegramUpdateWorkerOwnerRuntimeDeps<TContext>): TelegramUpdateWorkerOwnerRuntime<TContext>;
|
|
1221
1222
|
export interface TelegramUpdateAdmissionRuntimeBinding<TContext> {
|
|
@@ -3177,9 +3177,11 @@ export function createTelegramUpdateWorkerOwnerRuntime(deps) {
|
|
|
3177
3177
|
deps.dispatchNext(ctx);
|
|
3178
3178
|
deps.requestQueueHandoffReconciliation(ctx);
|
|
3179
3179
|
},
|
|
3180
|
-
onUpdateCompleted(
|
|
3181
|
-
if (deps.isContextCurrent(ctx))
|
|
3182
|
-
|
|
3180
|
+
onUpdateCompleted(updateId, ctx) {
|
|
3181
|
+
if (!deps.isContextCurrent(ctx))
|
|
3182
|
+
return;
|
|
3183
|
+
deps.dispatchNext(ctx);
|
|
3184
|
+
deps.afterUpdateCompleted?.(updateId);
|
|
3183
3185
|
},
|
|
3184
3186
|
};
|
|
3185
3187
|
}
|
|
@@ -42,7 +42,7 @@ Keep this boundary explicit:
|
|
|
42
42
|
|
|
43
43
|
### Instance, Session, And Context Cost
|
|
44
44
|
|
|
45
|
-
A Telegram destination follows a Pi instance, not an immutable Pi session file. Ordinary Telegram prompts enter whichever session is active in that assigned instance when dispatch occurs. If the operator replaces or resumes a session locally, pi-telegram rebinds its session-scoped runtime state while preserving the instance's Telegram target where supported. Telegram
|
|
45
|
+
A Telegram destination follows a Pi instance, not an immutable Pi session file. Ordinary Telegram prompts enter whichever session is active in that assigned instance when dispatch occurs. If the operator replaces or resumes a session locally, pi-telegram rebinds its session-scoped runtime state while preserving the instance's Telegram target where supported. Telegram exposes compaction and new-session replacement for the active session: `/new` first completes and removes its exact durable update, then dispatches an internal registered Pi command through `pi.sendUserMessage(..., { expandPromptTemplates: true })` so the handler runs with a real `ExtensionCommandContext` and calls the same `AgentSessionRuntime.newSession()` path as the terminal. Before invoking that path, the current transport owner CAS-publishes one expiring replacement intent inside the existing profile target snapshot. The intent discriminates `workspace-thread` from `classic-chat`. A same- or cross-process successor may consume either only for the exact profile, CWD, source session, target, and fresh lifetime: Threaded continuity re-keys the retained Workspace binding and preserves Thread, slot, and display identity, while Classic continuity preserves the exact chat target without creating a Workspace binding or entering topic lifecycle APIs. A confirmed callback is acknowledged and its dialog is deleted before session replacement begins. Once the successor has delivery authority, it atomically claims the intent by CAS-clearing it and then sends one separate terminal success result with bounded in-process transport retry. The old `withSession` path never publishes success, and an unclaimed intent never sends, preventing duplicate terminal notices across same-process startup and cleanup-ack ambiguity. Mismatch or expiry never authorizes binding takeover. Resume, fork, tree navigation, session switching, and full reload remain outside the stable Telegram API until Pi exposes safe public extension hooks for them.
|
|
46
46
|
|
|
47
47
|
`/telegram-connect` never launches a hidden or headless Pi process. A long-lived background Pi process can own Telegram only when something else explicitly launched that process and it satisfies the normal lock/runtime rules. Pi `print` and `json` modes stay passive and exit rather than becoming hidden polling owners.
|
|
48
48
|
|
|
@@ -70,7 +70,7 @@ The repository uses a **Flat Domain DAG**:
|
|
|
70
70
|
- `sync`: demand-driven Telegram reconciliation, mutable sync-slice state, nested provisioning activity, and local assumption policy. It does not own a complete Telegram bot read-model; Bot API lacks a complete topic/thread listing surface. It owns sync slices, invalidation triggers, config-persist invalidation sequencing, exact-target admitted stale-topic API recovery, observation intake, status/debug freshness, paired manual-disconnect/session-restart cleanup assembly, and reconciliation scheduling across bot identity, pairing assumptions, live target bindings, reservations, and transport health after meaningful observable signals. Production topic lifecycle and disconnect/restart cleanup enter profile-wide admission before the shared Workspace operation gate. Lifecycle admission spans observation-driven store settlement; cleanup admission spans intent publication, Telegram cleanup, binding mutation, durable settlement, and transport release. It should call narrower domain primitives rather than letting `index.ts`, `threads`, or `status` accumulate cross-cutting reconciliation policy.
|
|
71
71
|
- `thread-reconciler`: Threaded Mode control-plane planning for Telegram thread/tab lifecycle. It owns the reconciliation state machine (`stable`, `provisioning`, `sync-required`, `cleanup-required`), pure plans, proof-before-delete rules, pending-provision protection, fresh-creation grace windows, leader-epoch checks, and the single policy authority for destructive thread cleanup actions. It excludes live Telegram API calls, inbound routing, menu rendering, and direct persistence.
|
|
72
72
|
- `thread-cleanup-manager`: Disconnected proof-only admission planning for manual inactive-Workspace cleanup. It emits exact profile/binding/target snapshots only when durable inactivity exists, all live-owner/accepted-work/delivery evidence is `clear`, identities are unique, and no reservation, provision, or cleanup competes. Missing, malformed, duplicate, or `unknown` evidence returns no candidates; age and ordering never create authority. Its disconnected bounded profile/token-scoped work store atomically persists exact candidate snapshots, records one exact Workspace-issued deletion permit as outcome-unknown, rejects mismatched permits, and confirms deleted state idempotently; strict reads reject malformed/private-file ambiguity. A disconnected executor requires an injected exclusive Workspace deletion boundary across fresh planner evidence, exact retained-snapshot comparison, permit acquisition/recording, delete callback, and confirmation. The future fence owner must acquire and recheck in admission-ledger order; a retirement fence cannot be nested inside an active ordinary admission lease. Regressions prove drift stops before permit, unavailable/already-issued state cannot fabricate authority, and ambiguous delete remains outcome-unknown without replay. A production-shaped adapter preserves full Workspace records through external-protection resolution, then snapshots exact cleanup fields, reservations, provisions, and cleanup intents; protection exceptions become `unknown`, while source failures propagate fail-closed. Production Settings exposes **Review inactive tabs** only: profile-wide admission surrounds fresh evidence capture, one canonical 128-bit-digest work-set is retained, and a separate summary reports proven count, explicit no-deletion state, and Back navigation. The exact confirmation callback fits Telegram's 64-byte bound and accepts only canonical work-set IDs. **Clean inactive tabs** renders only when composition supplies a destructive port; production intentionally omits it, so malformed/stale callbacks fail closed and review remains non-destructive. Permit composition must not call `acquireRetirementFence()` because retirement remains pressure-only. The one admission-ledger fence now carries discriminated `pressure-retirement | manual-thread-cleanup` authority, treats legacy missing kind as pressure, exposes `acquireThreadCleanupFence()`, includes kind in exact comparison/permits, and remains profile-singleton. Cleanup-specific adopt/issue/absence/release/complete APIs preserve existing retirement callers and reject cross-kind use. Review admission releases before cleanup-fence acquisition; that fence then spans exact full-record/protection revalidation, sole permit issuance, work-set recording, one delete attempt, absence confirmation, binding/work-set commit, and fence completion. The v1-compatible schema and kind-specific acquire/adopt/issue/absence/release/complete methods are implemented; pressure methods reject manual fences and cleanup methods reject pressure fences. A disconnected permit runtime acquires the cleanup fence, revalidates under it, releases drifted unissued fences, refuses already-issued replay, and retains `commit-ready` until an injected durable commit succeeds. `threads.commitInactiveWorkspaceCleanup()` now removes only an exact full inactive binding after rechecking local records, claims, reservations, provisions, cleanups, and retirement intents; the retained cleanup candidate carries sufficient exact cwd/workspace/instance/global-slot/binding/target/inactivity/update commit identity, and under the retained fence exact absence closes commit-unknown retry without reconstructing the deleted full binding. Commit composition removes the binding before confirming the work-set, and failure retains `commit-ready`. A hidden coordinator validates canonical review identity, resolves the full binding, records the sole permit before one injected delete call, then commits binding, work-set, and fence. A `commit-ready` retry finishes without another delete; ambiguous deletion remains `deletion-issued` and is never replayed. A typed Settings-port adapter exposes this coordinator only when explicitly supplied and reports deleted, outcome-unknown, and blocked counts. Fake-port tests exercise the callback lifecycle and successor recovery: takeover requires injected proof, exact fence identity is preserved, live/unverifiable predecessors fail closed, and `commit-ready` resumes without deletion replay. Cross-process workers prove stale `prepared` contenders call fake transport once: deletion requires successful work-set permit CAS, and redundant fences over deleted entries settle without replay. Work-set pre-rename failure and lost post-rename acknowledgement retain recoverable fence truth; durable `deleted` completes that fence without another transport call. Binding-snapshot pre-rename ownership loss reloads and restores the exact binding, while post-rename acknowledgement loss reloads durable absence as successful commit. Cleanup runtime composition additionally requires candidate/work-set/runtime/fence profile equality before mutation and captures one stable successor owner snapshot for adoption. The optional Settings port reports only redacted exact-authority recovery classes: commit pending, deletion outcome unknown with no retry, or unavailable authority. It exposes no target, path, token, or transport details. Production still omits `cleanInactiveThreads`, so Bot API activation remains separate.
|
|
73
|
-
- `thread-display`: Shared Letters/Names/Directories projection, initial-create title selection, and serialized leader-owned title application. Owns bounded path disambiguation, ambiguous-label rejection, and profile/mode/epoch plus captured live-binding fences. Acknowledged `displayTitle` remains distinct from stable `threadName`; the caller owns triggers and live-owner discovery. Leader provisioning applies the configured projection before creation; registration ACKs deliver the acknowledged title before initial follower status, and heartbeat ACKs carry later changes. Stable runtime names remain restoration identity rather than presentation. Current-thread/TUI display identity is separate from restoration identity. Live bot choosers, notices, prompt labels, and cross-instance agent-target resolution use acknowledged display titles while captured numeric targets and live registrations remain the routing authority. Settings routes direct-owner changes locally and follower changes through an authenticated capability-gated envelope. The leader serializes preference writes and application.
|
|
73
|
+
- `thread-display`: Shared Letters/Names/Directories projection, Settings-mode/manual-override coordination, initial-create title selection, and serialized leader-owned title application. Owns bounded path disambiguation, ambiguous-label rejection, and profile/mode/epoch plus captured live-binding fences. Acknowledged `displayTitle` remains distinct from stable `threadName`; the caller owns triggers and live-owner discovery. Leader provisioning applies the configured projection before creation; registration ACKs deliver the acknowledged title before initial follower status, and heartbeat ACKs carry later changes. Stable runtime names remain restoration identity rather than presentation. Current-thread/TUI display identity is separate from restoration identity. Live bot choosers, notices, prompt labels, and cross-instance agent-target resolution use acknowledged display titles while captured numeric targets and live registrations remain the routing authority. Settings routes direct-owner changes locally and follower changes through an authenticated capability-gated envelope. The leader serializes preference writes and application.
|
|
74
74
|
- `workspace-slots`: Pure bounded global-letter selection and pressure-reclamation proposals. It consumes explicit protection/inactivity evidence and never discovers owners, persists state, or performs deletion.
|
|
75
75
|
- `workspace-admission`: Durable profile-scoped reader/writer ledger for cross-process exact-target, chat-wide, and profile-wide admission leases plus one destructive retirement fence. It owns atomic lease/fence transactions, proven-dead process-birth recovery, conservative malformed/ambiguous-state handling, exact successor adoption, retained-slot projection, and the durable `fenced` → `deletion-issued` → `commit-ready` phases that emit at most one deletion permit. Its runtime binding resolves `workspace-admission[.<profile>].json`, stores only the token SHA-256 profile authority, preserves separate named-profile identities across switching, permits changed-token rebind only when the prior ledger is provably empty, and fails closed while foreign leases or a fence remain. Issued fences cannot be released before confirmed absence and durable retirement commit; callers own journal/API/provisioning operations and retirement policy. Production composition supplies admission to journals, JSON/multipart API, leader/follower mutations, topic lifecycle, reroute restoration/reclamation, manual disconnect/session-restart cleanup, exact stale-target recovery, and Thread-store slot reservations; journal-evidence pruning also requires caller-supplied admission. Common async runners and the API adapter reject concurrent reuse of a live operation ID before a second caller can share or release its lease; once the first invocation exits, retry-stable recovery remains available. A 2/2 same-model independent post-fix quorum verified complete production-mutation composition at 0.96 confidence per reviewer. Destructive retirement remains disconnected by release scope; this verification does not authorize live deletion or replace disposable operator smoke.
|
|
76
76
|
- `workspace-retirement`: Profile/leader-fenced pressure preparation over the store snapshot. It counts standalone reservations, selects one candidate only at full slot capacity, rechecks protection, and persists/resumes an exact durable intent. Workspace bindings accumulate their historical follower-journal routing keys and distinguish complete fresh metadata from incomplete legacy evidence. Its read-only accepted-work policy resolves those binding-specific journals plus the shared leader journal and combines them with local exact targets, failing closed when source coverage or target decoding is incomplete. It can prune a known empty follower-journal key only from complete readable evidence plus explicit writer quiescence under exact binding/profile/epoch fences and an exact-target admission lease held through durable publication. Incomplete legacy bindings consume discovered hashed journals as target-scoped evidence but remain incomplete so every later retirement repeats discovery. The shared Workspace operation runtime serializes topic lifecycle, reroute restoration/reclamation, provisioning, delayed post-provision reconciliation, follower/manual cleanup, rename, and display mutation through one exposed gate. Detached mutation work must reacquire fresh admission rather than inherit a lease already released by its caller. A successor may durably adopt one exact stale-epoch intent after profile/binding/protection revalidation; direct old-epoch execution remains blocked. The isolated executor consumes that gate and requires the durable admission ledger. It acquires or exactly adopts the matching fence, rechecks protection after admissions close, advances to `deletion-issued` before invoking an executor-only `deleteForumTopic` port with the sole permit, and never reissues from that phase. Success or exact absence advances to `commit-ready`; store commit failure retains the fence, and exact completion follows durable binding+intent removal. A successor resolves an issued unknown outcome only through a separate exact-absence probe. Leader composition exposes exact registry, active/queued work, known journals, and profile-exact legacy discovery as protection evidence. Missing queue targets and incomplete reads remain unknown. The common direct Bot API client counts exact JSON/multipart targets until settlement; message-scoped edits/deletes without a thread conservatively protect every binding in their chat. Known historical follower owner keys decode to process-birth identity before liveness checks. Durable intents block matching claims and binding mutations. Preparation/adoption/execution remains disconnected from leader runtime by release scope. Independent review cleared the admission-composition blocker; live deletion and operator acceptance remain separate gates.
|
|
@@ -596,7 +596,7 @@ The concrete runtime and wire reference remains in [Generative Apps Runtime For
|
|
|
596
596
|
- Voice/STT/TTS providers: [Voice Integration](./voice.md).
|
|
597
597
|
- Inbound/outbound command-template handlers: [Command Templates](./command-templates.md).
|
|
598
598
|
|
|
599
|
-
Extension callbacks must avoid `pi-telegram` owned prefixes such as `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`. Workflow-specific Telegram slash commands should use the public command registry instead of becoming new core built-ins unless they are bridge lifecycle, transport ownership, queue safety, or essential operator controls.
|
|
599
|
+
Extension callbacks must avoid `pi-telegram` owned prefixes such as `compact:`, `new:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`. Workflow-specific Telegram slash commands should use the public command registry instead of becoming new core built-ins unless they are bridge lifecycle, transport ownership, queue safety, or essential operator controls.
|
|
600
600
|
|
|
601
601
|
The bridge does not mirror arbitrary `ctx.ui.confirm/input/select/custom` prompts from other extensions into Telegram. Companion extensions that need Telegram operation should expose a Telegram-native command, section, settings row, callback, status line, inbound/update handler, or assistant action-markup path instead of relying on hidden TUI-only prompts.
|
|
602
602
|
|
|
@@ -20,7 +20,7 @@ myext:page:2
|
|
|
20
20
|
|
|
21
21
|
- Use a stable extension-owned namespace, preferably the package or extension name without scope punctuation.
|
|
22
22
|
- Keep the namespace lowercase ASCII: `a-z`, `0-9`, `_`, `-`.
|
|
23
|
-
- Do not use `pi-telegram` owned prefixes: `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, `section:`. Current app navigation uses `menu:`; `status:` remains reserved for legacy/owned status callbacks but is not emitted by current UI. `compact:`
|
|
23
|
+
- Do not use `pi-telegram` owned prefixes: `compact:`, `new:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, `section:`. Current app navigation uses `menu:`; `status:` remains reserved for legacy/owned status callbacks but is not emitted by current UI. `compact:` and `new:` are owned by their destructive-action confirmation dialogs. `section:` is owned by the Extension Sections platform (0.10.0+), documented in [Extension Sections](./sections.md). `settings:` is owned for the built-in Settings submenu.
|
|
24
24
|
- Keep the full `callback_data` within Telegram's 64-byte limit.
|
|
25
25
|
- Put only opaque ids or small enum values in payloads; do not store secrets, full prompts, or large state.
|
|
26
26
|
- Treat callbacks as untrusted input. Validate namespace, action, and payload before executing side effects.
|
|
@@ -51,6 +51,7 @@ Stable commands inside Pi:
|
|
|
51
51
|
Stable commands inside the paired Telegram DM:
|
|
52
52
|
|
|
53
53
|
- `/start` — pair when needed and open the main application menu.
|
|
54
|
+
- `/new` — after idle and empty-queue checks, request a new Pi session in the current classic chat or Thread. The bridge acknowledges the callback, deletes its confirmation, completes and removes the exact durable update, then dispatches an internal Pi command through `pi.sendUserMessage(..., { expandPromptTemplates: true })`. Its real `ExtensionCommandContext` calls `ctx.newSession()`; one discriminated durable intent preserves either exact classic Profile/CWD/session/chat continuity or the Thread binding with slot/name re-key. The successor CAS-claims that intent before sending one terminal result. Busy, identity-mismatch, and unavailable-host paths fail closed.
|
|
54
55
|
- `/compact` — open confirmation and compact when idle.
|
|
55
56
|
- `/next` — abort active work first when needed, let the interrupted prompt receive its abort notice, then reply `Dispatching next queued turn.` to the exact queued prompt selected for the next model turn. The command itself is never the lifecycle-notice reply target, and aborted pending assistant text is suppressed.
|
|
56
57
|
- `/continue` — enqueue a priority `continue` prompt.
|
|
@@ -59,7 +60,7 @@ Stable commands inside the paired Telegram DM:
|
|
|
59
60
|
|
|
60
61
|
Hidden compatibility shortcuts may open sections directly: `/help`, `/status`, `/model`, `/thinking`, `/queue`, and `/settings`.
|
|
61
62
|
|
|
62
|
-
This command surface is a mobile companion subset, not a raw terminal-command bridge or session browser. A Telegram destination follows its assigned Pi instance and sends prompts into that instance's currently active session; it is not permanently bound to one session identity. Compaction
|
|
63
|
+
This command surface is a mobile companion subset, not a raw terminal-command bridge or session browser. A Telegram destination follows its assigned Pi instance and sends prompts into that instance's currently active session; it is not permanently bound to one session identity. Compaction and new-session replacement operate on the current session; resume, fork, tree navigation, session switching, TUI transcript clearing, and arbitrary slash-command dispatch stay out of the stable Telegram API unless Pi exposes safe public extension hooks for them.
|
|
63
64
|
|
|
64
65
|
### Tools and assistant-authored actions
|
|
65
66
|
|
|
@@ -120,7 +121,7 @@ Bot/session identity always persists under `profiles.<name>`. The ordinary setup
|
|
|
120
121
|
|
|
121
122
|
The file is global across Pi instances and contains configuration only. The per-profile polling/admission cursor is `acceptedThroughUpdateId` in that profile's private durable update journal; it is not a config key. On first connection after this cut, a legacy config cursor is transferred directly into the journal before polling and then removed from config. Journal publication failure preserves the legacy source; config publication failure leaves the journal authoritative so retry is idempotent. Cooperating instances serialize recursive config delta merges through `telegram.json.transaction` and preserve unrelated global/profile changes from newer disk snapshots. A semantically unchanged merge adopts the latest disk state in memory without replacing the file; later commits win when two deltas intentionally change the same leaf. Same-parent temp-file replacement retries bounded transient `EPERM`, `EACCES`, and `EBUSY` destination contention without deleting the live config or leaving transaction serialization. For manual edits, stop or idle the connected instances, publish a complete valid file atomically, and let them reload. A non-transactional editor racing Pi persistence has no same-leaf conflict guarantee.
|
|
122
123
|
|
|
123
|
-
Threaded Mode Settings exposes **Thread display** as Letters (default), Names, `directory-snake`, or `directory-title`; retained `directories` is unsupported, resolves to Letters, and is never rewritten automatically. `profiles.<name>.threadDisplayMode` is profile-scoped; absent and invalid values resolve to `letters`. Settings previews the current binding set through the same projector used for fresh titles and reconciliation. Names projects the generated dictionary name for the slot; the two directory modes tokenize Unicode path segments deterministically and render snake case or humanized title case, with `_a` or ` A` slot suffixes when required. The leader serializes preference persistence and title reconciliation, while a follower sends an authenticated `follower.setThreadDisplayMode` request gated by `thread-display-mode-v1`, the additional `directory-display-format-v1` capability for either new directory mode, and its exact registration generation. Config writes check the originating authority inside the config transaction; mode changes preserve target IDs, slots, generated recovery names, manual overrides, and queue ownership. `/name` mutations carry their originating target through final binding validation. The caller confirms only after application succeeds. A partial failure may leave the preference saved and some titles updated; Settings reports that state and permits retry. Acknowledged follower titles arrive through heartbeat rather than a new read loop.
|
|
124
|
+
Threaded Mode Settings exposes **Thread display** as Letters (default), Names, `directory-snake`, or `directory-title`; retained `directories` is unsupported, resolves to Letters, and is never rewritten automatically. `profiles.<name>.threadDisplayMode` is profile-scoped; absent and invalid values resolve to `letters`. Settings previews the current binding set through the same projector used for fresh titles and reconciliation. A manual `/name` override appears as `custom`; choosing any automatic Thread display option clears that override for the current Thread and immediately reapplies the selected projection. Names projects the generated dictionary name for the slot; the two directory modes tokenize Unicode path segments deterministically and render snake case or humanized title case, with `_a` or ` A` slot suffixes when required. The leader serializes preference persistence and title reconciliation, while a follower sends an authenticated `follower.setThreadDisplayMode` request gated by `thread-display-mode-v1`, the additional `directory-display-format-v1` capability for either new directory mode, and its exact registration generation. Config writes check the originating authority inside the config transaction; mode changes preserve target IDs, slots, generated recovery names, manual overrides, and queue ownership. `/name` mutations carry their originating target through final binding validation. The caller confirms only after application succeeds. A partial failure may leave the preference saved and some titles updated; Settings reports that state and permits retry. Acknowledged follower titles arrive through heartbeat rather than a new read loop.
|
|
124
125
|
|
|
125
126
|
Hidden/default semantics are represented by absence:
|
|
126
127
|
|
|
@@ -577,7 +578,7 @@ async function synthesizeDemoOgg(_text: string): Promise<string> {
|
|
|
577
578
|
|
|
578
579
|
## Callback Namespaces
|
|
579
580
|
|
|
580
|
-
Owned prefixes are reserved by `pi-telegram`: `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
|
|
581
|
+
Owned prefixes are reserved by `pi-telegram`: `compact:`, `new:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
|
|
581
582
|
|
|
582
583
|
Companion extensions should use their own short prefix for raw callbacks or use `ctx.callbackData()` inside sections. Unknown unowned callbacks may be forwarded to Pi as `[callback] <data>` after built-in handlers decline them.
|
|
583
584
|
|
|
@@ -197,7 +197,7 @@ The token is an implementation detail. Section authors **never** write `section:
|
|
|
197
197
|
1. Telegram update arrives through the single `pi-telegram` polling loop
|
|
198
198
|
2. Update handlers observe/consume (raw update interception)
|
|
199
199
|
3. Button action store (`tgbtn:*`)
|
|
200
|
-
4.
|
|
200
|
+
4. New-session and compact confirmation callbacks (`new:*`, `compact:*`)
|
|
201
201
|
5. Queue menu callbacks (`queue:*`)
|
|
202
202
|
6. Settings menu callbacks (`settings:*`)
|
|
203
203
|
7. Section callbacks (`section:*`)
|
|
@@ -372,7 +372,7 @@ section:0:settings:open → open settings root
|
|
|
372
372
|
section:0:<action>:<payload> → forwarded to handleCallback
|
|
373
373
|
```
|
|
374
374
|
|
|
375
|
-
`section:` is listed in `TELEGRAM_OWNED_CALLBACK_PREFIXES` alongside `compact:`, `menu:`, `model:`, `settings:`, `status:`, `tgbtn:`, `thinking:`, `queue:`. Layered extensions must not use this prefix.
|
|
375
|
+
`section:` is listed in `TELEGRAM_OWNED_CALLBACK_PREFIXES` alongside `compact:`, `new:`, `menu:`, `model:`, `settings:`, `status:`, `tgbtn:`, `thinking:`, `queue:`. Layered extensions must not use this prefix.
|
|
376
376
|
|
|
377
377
|
### Inline keyboard layout
|
|
378
378
|
|
|
@@ -48,7 +48,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
48
48
|
| `▶️` | Play / continue immediately | Idle `/next` result, `/continue` command, and matching menu action | Means work can start or resume directly without first aborting an active turn. |
|
|
49
49
|
| `⏹️` | Abort current Pi work | `/abort` command description and active `/stop` result | Stops active work; accompanying copy states separately when queued work is cleared. |
|
|
50
50
|
| `🟥` | Destructive stop command | `/stop` command description | Strong warning at the command/action entrypoint; standalone results use the more precise idle or abort state icon. |
|
|
51
|
-
| `🆕` | New session / fresh start |
|
|
51
|
+
| `🆕` | New session / fresh start | `/new`, session replacement notices | `/new` replaces the active Pi session while preserving the current classic chat or Thread target; use it only for a real session reset. |
|
|
52
52
|
| `🔄` | Refresh | Queue refresh row and future refresh buttons | Re-fetch/re-render current surface, not transport reconnect. |
|
|
53
53
|
| `↪️` | Reroute to an existing target | Thread chooser buttons that send a captured command/message from one thread to another live thread | Curved arrow means the message arrived here but bends to another target. |
|
|
54
54
|
| `🔁` | Replace/restore mode | Thread replace/restore chooser entrypoints | Opens a second step for moving a Pi instance binding to the current source thread. |
|
|
@@ -228,6 +228,7 @@ Rules:
|
|
|
228
228
|
- Format standalone notices as one fully bold line: relevant emoji, one space, concise sentence, and terminal period. Menu or chooser headings use the same fully bold form but end in a colon when controls or detail follow. Empty-queue headings are the deliberate exception: fully bold, with no trailing period or colon.
|
|
229
229
|
- Keep the emoji and complete sentence or heading inside the single bold span; do not bold only a fragment. A material name or phrase may receive nested italic emphasis without breaking the outer bold hierarchy—for example `<b>📡 Instance <i>Cedar</i> connected.</b>`.
|
|
230
230
|
- Apply the same hierarchy to success, progress, empty, busy, unavailable, cancellation, and failure notices.
|
|
231
|
+
- Once an action has settled, describe only the completed result in completed-state language. Do not append transitional copy such as “returning” or “starting”; use a separate progress surface only while work is genuinely still pending.
|
|
231
232
|
- Callback alerts remain plain text because Telegram does not support rich text there, but still keep the relevant emoji and concise sentence.
|
|
232
233
|
- Setting detail cards may include an emoji in the heading, then a colon and the current value in `<code>`.
|
|
233
234
|
- Explain what the setting does and what the options mean only as much as needed.
|
|
@@ -150,7 +150,7 @@ This means:
|
|
|
150
150
|
|
|
151
151
|
- Extensions can claim callback namespaces that `pi-telegram` would otherwise forward as `[callback] <data>` text.
|
|
152
152
|
- Extensions can observe updates by always returning `"pass"`.
|
|
153
|
-
- Extensions must not consume updates that belong to `pi-telegram`'s own prefixes (`compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, `section:`) unless they are deliberately replacing that behavior.
|
|
153
|
+
- Extensions must not consume updates that belong to `pi-telegram`'s own prefixes (`compact:`, `new:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, `section:`) unless they are deliberately replacing that behavior.
|
|
154
154
|
|
|
155
155
|
## Ownership semantics
|
|
156
156
|
|
|
@@ -804,7 +804,9 @@ export function createTelegramActivityPublicationRuntime(): TelegramActivityPubl
|
|
|
804
804
|
|
|
805
805
|
export interface TelegramAssistantOutputRuntime {
|
|
806
806
|
start: () => void;
|
|
807
|
+
beginTurn: () => void;
|
|
807
808
|
accept: (event: TelegramAssistantSegmentEvent) => void;
|
|
809
|
+
hasAdmittedTelegramIntermediate: (text: string) => boolean;
|
|
808
810
|
waitForIdle: () => Promise<void>;
|
|
809
811
|
stop: () => void;
|
|
810
812
|
}
|
|
@@ -834,6 +836,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
|
|
|
834
836
|
let running = false;
|
|
835
837
|
let tail: Promise<void> = Promise.resolve();
|
|
836
838
|
const admitted = new Set<string>();
|
|
839
|
+
const admittedTelegramIntermediateText = new Set<string>();
|
|
837
840
|
const isEligibleEvent = (event: TelegramAssistantSegmentEvent): boolean =>
|
|
838
841
|
(event.source === "telegram" && event.placement === "intermediate") ||
|
|
839
842
|
event.source === "local" ||
|
|
@@ -845,13 +848,20 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
|
|
|
845
848
|
generation += 1;
|
|
846
849
|
running = true;
|
|
847
850
|
admitted.clear();
|
|
851
|
+
admittedTelegramIntermediateText.clear();
|
|
848
852
|
tail = Promise.resolve();
|
|
849
853
|
},
|
|
854
|
+
beginTurn() {
|
|
855
|
+
admittedTelegramIntermediateText.clear();
|
|
856
|
+
},
|
|
850
857
|
accept(event) {
|
|
851
858
|
if (!running || !isEligibleEvent(event) || !event.text.trim()) return;
|
|
852
859
|
const key = `${event.activityId}:${event.sequence}`;
|
|
853
860
|
if (admitted.has(key)) return;
|
|
854
861
|
admitted.add(key);
|
|
862
|
+
if (event.source === "telegram" && event.placement === "intermediate") {
|
|
863
|
+
admittedTelegramIntermediateText.add(event.text.trim());
|
|
864
|
+
}
|
|
855
865
|
const admittedGeneration = generation;
|
|
856
866
|
const admittedAuthority = deps.captureAuthority?.();
|
|
857
867
|
const preparation = deps.prepareSend?.(event);
|
|
@@ -877,6 +887,9 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
|
|
|
877
887
|
}
|
|
878
888
|
}).finally(() => preparation?.settle());
|
|
879
889
|
},
|
|
890
|
+
hasAdmittedTelegramIntermediate(text) {
|
|
891
|
+
return admittedTelegramIntermediateText.has(text.trim());
|
|
892
|
+
},
|
|
880
893
|
waitForIdle() {
|
|
881
894
|
return tail;
|
|
882
895
|
},
|
|
@@ -884,6 +897,7 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
|
|
|
884
897
|
generation += 1;
|
|
885
898
|
running = false;
|
|
886
899
|
admitted.clear();
|
|
900
|
+
admittedTelegramIntermediateText.clear();
|
|
887
901
|
},
|
|
888
902
|
};
|
|
889
903
|
}
|
|
@@ -835,7 +835,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
835
835
|
activityVerbosityRuntime?: ActivityVerbosity.TelegramActivityVerbosityRuntime;
|
|
836
836
|
assistantOutputRuntime: Pick<
|
|
837
837
|
Activity.TelegramAssistantOutputRuntime,
|
|
838
|
-
"start" | "waitForIdle" | "stop"
|
|
838
|
+
"start" | "beginTurn" | "hasAdmittedTelegramIntermediate" | "waitForIdle" | "stop"
|
|
839
839
|
>;
|
|
840
840
|
sessionLifecycleRuntime: Pick<
|
|
841
841
|
Lifecycle.TelegramLifecycleRegistrationDeps,
|
|
@@ -862,6 +862,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
862
862
|
deferredQueueDispatchRuntime: Queue.TelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>;
|
|
863
863
|
modelContextAvailabilityRuntime: Prompts.TelegramModelContextAvailabilityRuntime;
|
|
864
864
|
disconnectOnQuit?: () => Promise<unknown>;
|
|
865
|
+
onSessionStarted?: (event: Pi.SessionStartEvent, ctx: Pi.ExtensionContext) => void;
|
|
865
866
|
shutdownGenerativeAppLiveSurfaces?: () => void;
|
|
866
867
|
resolveAutomaticThreadCleanupEnabled?: () => boolean | Promise<boolean>;
|
|
867
868
|
buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
|
|
@@ -952,6 +953,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
952
953
|
deferredQueueDispatchRuntime,
|
|
953
954
|
modelContextAvailabilityRuntime,
|
|
954
955
|
disconnectOnQuit,
|
|
956
|
+
onSessionStarted,
|
|
955
957
|
shutdownGenerativeAppLiveSurfaces,
|
|
956
958
|
resolveAutomaticThreadCleanupEnabled,
|
|
957
959
|
buttonActionStore,
|
|
@@ -1136,7 +1138,10 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1136
1138
|
clearDispatchPending: lifecycle.clearDispatchPending,
|
|
1137
1139
|
setFoldQueuedPromptsIntoHistory: lifecycle.setFoldQueuedPromptsIntoHistory,
|
|
1138
1140
|
setActiveTurn: activeTurnRuntime.set,
|
|
1139
|
-
onPromptHandedOff,
|
|
1141
|
+
onPromptHandedOff: (turn, ctx) => {
|
|
1142
|
+
assistantOutputRuntime.beginTurn();
|
|
1143
|
+
onPromptHandedOff?.(turn, ctx);
|
|
1144
|
+
},
|
|
1140
1145
|
createPreviewState: previewRuntime.resetState,
|
|
1141
1146
|
startTypingLoop: (ctx) => {
|
|
1142
1147
|
const turn = activeTurnRuntime.get();
|
|
@@ -1148,6 +1153,8 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1148
1153
|
getActiveTurn: activeTurnRuntime.get,
|
|
1149
1154
|
loadConfig: configStore.load,
|
|
1150
1155
|
extractAssistant: Replies.extractRunAssistantMessage,
|
|
1156
|
+
isRecoveredAssistantAlreadyPublished: (assistant) =>
|
|
1157
|
+
!!assistant.text && assistantOutputRuntime.hasAdmittedTelegramIntermediate(assistant.text),
|
|
1151
1158
|
getFoldQueuedPromptsIntoHistory:
|
|
1152
1159
|
lifecycle.shouldFoldQueuedPromptsIntoHistory,
|
|
1153
1160
|
resetRuntimeState: agentEndResetter,
|
|
@@ -1273,6 +1280,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1273
1280
|
activityVerbosityRuntime?.reset();
|
|
1274
1281
|
modelContextAvailabilityRuntime.reconcile();
|
|
1275
1282
|
await sessionLifecycleRuntime.onSessionStart(event, ctx);
|
|
1283
|
+
onSessionStarted?.(event, ctx);
|
|
1276
1284
|
},
|
|
1277
1285
|
async onSessionShutdown(event, ctx) {
|
|
1278
1286
|
if (!isSessionContextActive(ctx)) return;
|