@llblab/pi-telegram 0.24.6 → 0.24.7
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/AGENTS.md +5 -5
- package/BACKLOG.md +0 -12
- package/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/multi-instance-bus.md +7 -6
- package/docs/outbound.md +1 -0
- package/docs/public-api.md +1 -1
- package/docs/ui-style.md +4 -0
- package/index.ts +10 -0
- package/lib/bus-leader.ts +204 -59
- package/lib/bus.ts +6 -1
- package/lib/config.ts +1 -1
- package/lib/keyboard.ts +1 -0
- package/lib/menu-settings.ts +8 -12
- package/lib/outbound-buttons.ts +34 -2
- package/lib/outbound.ts +1 -0
- package/lib/routing.ts +25 -2
- package/lib/sync.ts +42 -20
- package/lib/thread-reconciler.ts +100 -2
- package/lib/threads.ts +95 -0
- package/package.json +3 -26
- package/scripts/audit-dependencies.ts +0 -78
- package/scripts/dependency-audit-policy.ts +0 -300
package/lib/sync.ts
CHANGED
|
@@ -110,11 +110,19 @@ export interface TelegramLeaderHealthRuntime {
|
|
|
110
110
|
export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
111
111
|
instanceId: string;
|
|
112
112
|
getCurrentThreadRecord: () =>
|
|
113
|
-
| {
|
|
113
|
+
| {
|
|
114
|
+
target: TelegramTarget;
|
|
115
|
+
instanceId?: string;
|
|
116
|
+
profileKey?: string;
|
|
117
|
+
owner?: { kind?: string };
|
|
118
|
+
}
|
|
114
119
|
| undefined;
|
|
115
120
|
topicTargetStore: Pick<
|
|
116
121
|
TelegramTopicTargetStore,
|
|
117
|
-
|
|
122
|
+
| "markStaleByTarget"
|
|
123
|
+
| "persist"
|
|
124
|
+
| "upsertPendingCleanup"
|
|
125
|
+
| "removePendingCleanup"
|
|
118
126
|
>;
|
|
119
127
|
callApi: <TResponse>(
|
|
120
128
|
method: string,
|
|
@@ -175,22 +183,44 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
175
183
|
}
|
|
176
184
|
}
|
|
177
185
|
} else {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
186
|
+
const target = currentRecord.target as TelegramTarget & {
|
|
187
|
+
threadId: number;
|
|
188
|
+
};
|
|
189
|
+
const runtimeGeneration = currentRecord.instanceId ?? deps.instanceId;
|
|
190
|
+
const intent: ThreadReconciler.TelegramThreadCleanupIntent = {
|
|
191
|
+
id: `cleanup:${deps.instanceId}:${runtimeGeneration}:${target.chatId}:${target.threadId}`,
|
|
192
|
+
owner: isManualFollower ? "manual-follower" : "leader",
|
|
193
|
+
instanceId: deps.instanceId,
|
|
194
|
+
runtimeGeneration,
|
|
195
|
+
...(currentRecord.profileKey
|
|
196
|
+
? { profileKey: currentRecord.profileKey }
|
|
197
|
+
: {}),
|
|
198
|
+
target,
|
|
199
|
+
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
200
|
+
};
|
|
201
|
+
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
202
|
+
await deps.topicTargetStore.persist();
|
|
182
203
|
const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(
|
|
183
|
-
ThreadReconciler.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
leaderEpoch,
|
|
204
|
+
ThreadReconciler.planThreadReconciliation({
|
|
205
|
+
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
206
|
+
currentLeaderEpoch: leaderEpoch,
|
|
207
|
+
records: [],
|
|
208
|
+
pendingCleanups: [intent],
|
|
189
209
|
}),
|
|
190
210
|
{
|
|
191
211
|
callApi(method, body) {
|
|
192
212
|
return deps.callApi(method, body);
|
|
193
213
|
},
|
|
214
|
+
markStaleByTarget(targetToMark, syncStatus, lastSyncError) {
|
|
215
|
+
return deps.topicTargetStore.markStaleByTarget(
|
|
216
|
+
targetToMark,
|
|
217
|
+
syncStatus,
|
|
218
|
+
lastSyncError,
|
|
219
|
+
);
|
|
220
|
+
},
|
|
221
|
+
removeCleanupIntentById(id) {
|
|
222
|
+
return deps.topicTargetStore.removePendingCleanup(id);
|
|
223
|
+
},
|
|
194
224
|
persist() {
|
|
195
225
|
return deps.topicTargetStore.persist();
|
|
196
226
|
},
|
|
@@ -203,14 +233,6 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
203
233
|
"Telegram thread deletion was not confirmed; inspect /telegram-status --debug and retry /telegram-disconnect.",
|
|
204
234
|
);
|
|
205
235
|
}
|
|
206
|
-
if (!stillOwnsLeaderEpoch()) return deps.stopPolling();
|
|
207
|
-
const offlineChanged =
|
|
208
|
-
deps.topicTargetStore.markOfflineByInstanceId(deps.instanceId) > 0;
|
|
209
|
-
if (!stillOwnsLeaderEpoch()) return deps.stopPolling();
|
|
210
|
-
if (offlineChanged) {
|
|
211
|
-
await deps.topicTargetStore.persist();
|
|
212
|
-
if (!stillOwnsLeaderEpoch()) return deps.stopPolling();
|
|
213
|
-
}
|
|
214
236
|
}
|
|
215
237
|
const leaderTarget = deps.getLeaderTarget();
|
|
216
238
|
if (
|
package/lib/thread-reconciler.ts
CHANGED
|
@@ -48,6 +48,16 @@ export interface TelegramThreadPendingProvision {
|
|
|
48
48
|
leaderEpoch?: number | string;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export interface TelegramThreadCleanupIntent {
|
|
52
|
+
id: string;
|
|
53
|
+
owner: "leader" | "manual-follower";
|
|
54
|
+
instanceId: string;
|
|
55
|
+
runtimeGeneration: string;
|
|
56
|
+
profileKey?: string;
|
|
57
|
+
target: ThreadTarget;
|
|
58
|
+
requestedAtMs: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
export interface TelegramUnboundThreadMessageObservation {
|
|
52
62
|
target: TelegramTarget & { threadId: number };
|
|
53
63
|
observedAtMs: number;
|
|
@@ -130,6 +140,24 @@ export type ThreadReconciliationAction =
|
|
|
130
140
|
messageId?: number;
|
|
131
141
|
leaderEpoch?: number | string;
|
|
132
142
|
}
|
|
143
|
+
| {
|
|
144
|
+
kind: "close-delete-graceful-shutdown-topic";
|
|
145
|
+
target: TelegramTarget & { threadId: number };
|
|
146
|
+
reason: "graceful-shutdown";
|
|
147
|
+
cleanupIntentId: string;
|
|
148
|
+
instanceId: string;
|
|
149
|
+
runtimeGeneration: string;
|
|
150
|
+
leaderEpoch?: number | string;
|
|
151
|
+
}
|
|
152
|
+
| {
|
|
153
|
+
kind: "cancel-superseded-graceful-shutdown-cleanup";
|
|
154
|
+
target: TelegramTarget & { threadId: number };
|
|
155
|
+
reason: "replacement-registration";
|
|
156
|
+
cleanupIntentId: string;
|
|
157
|
+
instanceId: string;
|
|
158
|
+
runtimeGeneration: string;
|
|
159
|
+
leaderEpoch?: number | string;
|
|
160
|
+
}
|
|
133
161
|
| {
|
|
134
162
|
kind: "close-delete-expired-pending-provision-topic";
|
|
135
163
|
target: TelegramTarget & { threadId: number };
|
|
@@ -186,6 +214,7 @@ export interface ThreadReconciliationApplyPorts {
|
|
|
186
214
|
) => boolean;
|
|
187
215
|
persist?: () => Promise<void>;
|
|
188
216
|
removePendingProvisionById?: (id: string) => boolean;
|
|
217
|
+
removeCleanupIntentById?: (id: string) => boolean;
|
|
189
218
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
190
219
|
recordRuntimeEvent?: (
|
|
191
220
|
category: string,
|
|
@@ -201,6 +230,7 @@ export interface ThreadReconciliationInput {
|
|
|
201
230
|
reservations?: readonly ThreadReconciliationReservation[];
|
|
202
231
|
observations?: readonly ThreadReconciliationObservation[];
|
|
203
232
|
pendingProvisions?: readonly TelegramThreadPendingProvision[];
|
|
233
|
+
pendingCleanups?: readonly TelegramThreadCleanupIntent[];
|
|
204
234
|
unboundMessages?: readonly TelegramUnboundThreadMessageObservation[];
|
|
205
235
|
reservedMessages?: readonly TelegramReservedThreadMessageObservation[];
|
|
206
236
|
proactiveReservationCleanup?: boolean;
|
|
@@ -291,6 +321,8 @@ function isCleanupAction(action: ThreadReconciliationAction): boolean {
|
|
|
291
321
|
action.kind === "close-delete-replaced-follower-topic" ||
|
|
292
322
|
action.kind === "close-delete-previous-leader-topic" ||
|
|
293
323
|
action.kind === "close-delete-disconnected-instance-topic" ||
|
|
324
|
+
action.kind === "close-delete-graceful-shutdown-topic" ||
|
|
325
|
+
action.kind === "cancel-superseded-graceful-shutdown-cleanup" ||
|
|
294
326
|
action.kind === "close-delete-expired-pending-provision-topic"
|
|
295
327
|
);
|
|
296
328
|
}
|
|
@@ -505,6 +537,26 @@ export async function applyThreadReconciliationPlan(
|
|
|
505
537
|
shouldPersist;
|
|
506
538
|
continue;
|
|
507
539
|
}
|
|
540
|
+
if (action.kind === "cancel-superseded-graceful-shutdown-cleanup") {
|
|
541
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) {
|
|
542
|
+
incompleteActions.push(action);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const changed =
|
|
546
|
+
ports.removeCleanupIntentById?.(action.cleanupIntentId) ?? false;
|
|
547
|
+
shouldPersist = changed || shouldPersist;
|
|
548
|
+
ports.recordRuntimeEvent?.(
|
|
549
|
+
"telegram",
|
|
550
|
+
"Cancelled superseded Telegram topic cleanup",
|
|
551
|
+
{
|
|
552
|
+
phase: "thread-reconciler-cleanup-superseded",
|
|
553
|
+
instanceId: action.instanceId,
|
|
554
|
+
chatId: action.target.chatId,
|
|
555
|
+
threadId: action.target.threadId,
|
|
556
|
+
},
|
|
557
|
+
);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
508
560
|
if (action.kind === "close-stale-replaced-topic") {
|
|
509
561
|
if (shouldSkipForStaleLeaderEpoch(action, ports)) {
|
|
510
562
|
incompleteActions.push(action);
|
|
@@ -558,6 +610,7 @@ export async function applyThreadReconciliationPlan(
|
|
|
558
610
|
action.kind === "close-delete-replaced-follower-topic" ||
|
|
559
611
|
action.kind === "close-delete-previous-leader-topic" ||
|
|
560
612
|
action.kind === "close-delete-disconnected-instance-topic" ||
|
|
613
|
+
action.kind === "close-delete-graceful-shutdown-topic" ||
|
|
561
614
|
action.kind === "close-delete-expired-pending-provision-topic"
|
|
562
615
|
) {
|
|
563
616
|
if (shouldSkipForStaleLeaderEpoch(action, ports)) {
|
|
@@ -647,7 +700,9 @@ export async function applyThreadReconciliationPlan(
|
|
|
647
700
|
? "Previous leader Telegram topic deleted"
|
|
648
701
|
: action.kind === "close-delete-disconnected-instance-topic"
|
|
649
702
|
? "Disconnected instance Telegram topic deleted"
|
|
650
|
-
:
|
|
703
|
+
: action.kind === "close-delete-graceful-shutdown-topic"
|
|
704
|
+
? "Graceful shutdown Telegram topic deleted"
|
|
705
|
+
: "Expired pending provision Telegram topic deleted",
|
|
651
706
|
{
|
|
652
707
|
phase:
|
|
653
708
|
action.kind === "close-delete-unbound-topic"
|
|
@@ -661,7 +716,10 @@ export async function applyThreadReconciliationPlan(
|
|
|
661
716
|
: action.kind ===
|
|
662
717
|
"close-delete-disconnected-instance-topic"
|
|
663
718
|
? "thread-reconciler-disconnected-instance-topic-delete"
|
|
664
|
-
:
|
|
719
|
+
: action.kind ===
|
|
720
|
+
"close-delete-graceful-shutdown-topic"
|
|
721
|
+
? "thread-reconciler-graceful-shutdown-topic-delete"
|
|
722
|
+
: "thread-reconciler-expired-pending-provision-topic-delete",
|
|
665
723
|
chatId: action.target.chatId,
|
|
666
724
|
threadId: action.target.threadId,
|
|
667
725
|
...("messageId" in action ? { messageId: action.messageId } : {}),
|
|
@@ -678,6 +736,16 @@ export async function applyThreadReconciliationPlan(
|
|
|
678
736
|
if (changed) persistFences.push(action);
|
|
679
737
|
shouldPersist = changed || shouldPersist;
|
|
680
738
|
}
|
|
739
|
+
if (
|
|
740
|
+
action.kind === "close-delete-graceful-shutdown-topic" &&
|
|
741
|
+
deleteConfirmed
|
|
742
|
+
) {
|
|
743
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) continue;
|
|
744
|
+
const changed =
|
|
745
|
+
ports.removeCleanupIntentById?.(action.cleanupIntentId) ?? false;
|
|
746
|
+
if (changed) persistFences.push(action);
|
|
747
|
+
shouldPersist = changed || shouldPersist;
|
|
748
|
+
}
|
|
681
749
|
}
|
|
682
750
|
}
|
|
683
751
|
if (shouldPersist) {
|
|
@@ -736,6 +804,36 @@ export function planThreadReconciliation(
|
|
|
736
804
|
);
|
|
737
805
|
|
|
738
806
|
const actions: ThreadReconciliationAction[] = [];
|
|
807
|
+
for (const cleanup of input.pendingCleanups ?? []) {
|
|
808
|
+
const superseded = input.records.some(
|
|
809
|
+
(record) =>
|
|
810
|
+
isCurrentRecord(record) &&
|
|
811
|
+
targetKey(record.target) === targetKey(cleanup.target) &&
|
|
812
|
+
record.instanceId !== cleanup.instanceId,
|
|
813
|
+
);
|
|
814
|
+
const common = {
|
|
815
|
+
target: cleanup.target,
|
|
816
|
+
cleanupIntentId: cleanup.id,
|
|
817
|
+
instanceId: cleanup.instanceId,
|
|
818
|
+
runtimeGeneration: cleanup.runtimeGeneration,
|
|
819
|
+
...(input.currentLeaderEpoch !== undefined
|
|
820
|
+
? { leaderEpoch: input.currentLeaderEpoch }
|
|
821
|
+
: {}),
|
|
822
|
+
};
|
|
823
|
+
if (superseded) {
|
|
824
|
+
actions.push({
|
|
825
|
+
...common,
|
|
826
|
+
kind: "cancel-superseded-graceful-shutdown-cleanup",
|
|
827
|
+
reason: "replacement-registration",
|
|
828
|
+
});
|
|
829
|
+
} else {
|
|
830
|
+
actions.push({
|
|
831
|
+
...common,
|
|
832
|
+
kind: "close-delete-graceful-shutdown-topic",
|
|
833
|
+
reason: "graceful-shutdown",
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
739
837
|
for (const provision of input.pendingProvisions ?? []) {
|
|
740
838
|
if (!provision.target) continue;
|
|
741
839
|
if (!isPendingProvisionExpired(provision, input.nowMs)) continue;
|
package/lib/threads.ts
CHANGED
|
@@ -92,6 +92,9 @@ export interface TelegramThreadPendingProvision {
|
|
|
92
92
|
leaderEpoch?: number | string;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
export type TelegramThreadCleanupIntent =
|
|
96
|
+
ThreadReconciler.TelegramThreadCleanupIntent;
|
|
97
|
+
|
|
95
98
|
type TelegramProvisionRecoveryFile = Record<
|
|
96
99
|
string,
|
|
97
100
|
{
|
|
@@ -160,6 +163,7 @@ export interface TelegramTopicTargetFile {
|
|
|
160
163
|
identities?: TelegramThreadIdentityRecord[];
|
|
161
164
|
reservations?: TelegramThreadReservation[];
|
|
162
165
|
pendingProvisions?: TelegramThreadPendingProvision[];
|
|
166
|
+
pendingCleanups?: TelegramThreadCleanupIntent[];
|
|
163
167
|
syncObservations?: TelegramTopicSyncObservation[];
|
|
164
168
|
}
|
|
165
169
|
|
|
@@ -229,6 +233,7 @@ export interface TelegramTopicTargetStore {
|
|
|
229
233
|
) => { slot?: string; threadName?: string } | undefined;
|
|
230
234
|
listReservations: () => TelegramThreadReservation[];
|
|
231
235
|
listPendingProvisions: () => TelegramThreadPendingProvision[];
|
|
236
|
+
listPendingCleanups: () => TelegramThreadCleanupIntent[];
|
|
232
237
|
listSyncObservations: () => TelegramTopicSyncObservation[];
|
|
233
238
|
reserveThread: (reservation: TelegramThreadReservation) => void;
|
|
234
239
|
upsertPendingProvision: (provision: TelegramThreadPendingProvision) => void;
|
|
@@ -237,6 +242,8 @@ export interface TelegramTopicTargetStore {
|
|
|
237
242
|
target: TelegramTarget & { threadId: number },
|
|
238
243
|
) => Promise<boolean>;
|
|
239
244
|
removePendingProvision: (id: string) => boolean;
|
|
245
|
+
upsertPendingCleanup: (intent: TelegramThreadCleanupIntent) => void;
|
|
246
|
+
removePendingCleanup: (id: string) => boolean;
|
|
240
247
|
getBotState: () => TelegramBotStateSnapshot;
|
|
241
248
|
setBotState: (state: Partial<TelegramBotStateSnapshot>) => void;
|
|
242
249
|
setStatusSnapshot: (snapshot: {
|
|
@@ -876,6 +883,56 @@ function normalizePendingProvision(
|
|
|
876
883
|
};
|
|
877
884
|
}
|
|
878
885
|
|
|
886
|
+
function normalizePendingCleanup(
|
|
887
|
+
value: unknown,
|
|
888
|
+
): TelegramThreadCleanupIntent | undefined {
|
|
889
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
890
|
+
return undefined;
|
|
891
|
+
const record = value as Record<string, unknown>;
|
|
892
|
+
const targetValue = record.target;
|
|
893
|
+
if (
|
|
894
|
+
!targetValue ||
|
|
895
|
+
typeof targetValue !== "object" ||
|
|
896
|
+
Array.isArray(targetValue)
|
|
897
|
+
) {
|
|
898
|
+
return undefined;
|
|
899
|
+
}
|
|
900
|
+
const targetRecord = targetValue as Record<string, unknown>;
|
|
901
|
+
const owner = record.owner;
|
|
902
|
+
if (owner !== "leader" && owner !== "manual-follower") return undefined;
|
|
903
|
+
if (typeof record.id !== "string" || record.id.length === 0) return undefined;
|
|
904
|
+
if (typeof record.instanceId !== "string" || record.instanceId.length === 0)
|
|
905
|
+
return undefined;
|
|
906
|
+
if (
|
|
907
|
+
typeof record.runtimeGeneration !== "string" ||
|
|
908
|
+
record.runtimeGeneration.length === 0
|
|
909
|
+
) {
|
|
910
|
+
return undefined;
|
|
911
|
+
}
|
|
912
|
+
if (
|
|
913
|
+
typeof targetRecord.chatId !== "number" ||
|
|
914
|
+
typeof targetRecord.threadId !== "number" ||
|
|
915
|
+
!Number.isInteger(targetRecord.threadId) ||
|
|
916
|
+
typeof record.requestedAtMs !== "number"
|
|
917
|
+
) {
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
id: record.id,
|
|
922
|
+
owner,
|
|
923
|
+
instanceId: record.instanceId,
|
|
924
|
+
runtimeGeneration: record.runtimeGeneration,
|
|
925
|
+
...(typeof record.profileKey === "string"
|
|
926
|
+
? { profileKey: record.profileKey }
|
|
927
|
+
: {}),
|
|
928
|
+
target: {
|
|
929
|
+
chatId: targetRecord.chatId,
|
|
930
|
+
threadId: targetRecord.threadId,
|
|
931
|
+
},
|
|
932
|
+
requestedAtMs: record.requestedAtMs,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
879
936
|
function normalizeReservation(
|
|
880
937
|
value: unknown,
|
|
881
938
|
): TelegramThreadReservation | undefined {
|
|
@@ -970,6 +1027,12 @@ function parseTopicTargetFile(value: unknown): TelegramTopicTargetFile {
|
|
|
970
1027
|
return normalized ? [normalized] : [];
|
|
971
1028
|
})
|
|
972
1029
|
: [],
|
|
1030
|
+
pendingCleanups: Array.isArray(file.pendingCleanups)
|
|
1031
|
+
? file.pendingCleanups.flatMap((intent) => {
|
|
1032
|
+
const normalized = normalizePendingCleanup(intent);
|
|
1033
|
+
return normalized ? [normalized] : [];
|
|
1034
|
+
})
|
|
1035
|
+
: [],
|
|
973
1036
|
syncObservations: Array.isArray(file.syncObservations)
|
|
974
1037
|
? file.syncObservations.flatMap((observation) => {
|
|
975
1038
|
const normalized = normalizeSyncObservation(observation);
|
|
@@ -1084,6 +1147,7 @@ export function createTelegramTopicTargetStore(
|
|
|
1084
1147
|
let identities = new Map<string, TelegramThreadIdentityRecord>();
|
|
1085
1148
|
let reservations: TelegramThreadReservation[] = [];
|
|
1086
1149
|
let pendingProvisions: TelegramThreadPendingProvision[] = [];
|
|
1150
|
+
let pendingCleanups: TelegramThreadCleanupIntent[] = [];
|
|
1087
1151
|
let syncObservations: TelegramTopicSyncObservation[] = [];
|
|
1088
1152
|
let followerRecoveryHints = new Map<
|
|
1089
1153
|
string,
|
|
@@ -1140,6 +1204,7 @@ export function createTelegramTopicTargetStore(
|
|
|
1140
1204
|
identities = new Map();
|
|
1141
1205
|
reservations = [];
|
|
1142
1206
|
pendingProvisions = [];
|
|
1207
|
+
pendingCleanups = [];
|
|
1143
1208
|
syncObservations = [];
|
|
1144
1209
|
followerRecoveryHints = new Map();
|
|
1145
1210
|
statusSnapshot = {};
|
|
@@ -1157,6 +1222,7 @@ export function createTelegramTopicTargetStore(
|
|
|
1157
1222
|
identities = new Map();
|
|
1158
1223
|
reservations = [];
|
|
1159
1224
|
pendingProvisions = [];
|
|
1225
|
+
pendingCleanups = [];
|
|
1160
1226
|
syncObservations = [];
|
|
1161
1227
|
followerRecoveryHints = new Map();
|
|
1162
1228
|
loaded = true;
|
|
@@ -1207,6 +1273,10 @@ export function createTelegramTopicTargetStore(
|
|
|
1207
1273
|
: {}),
|
|
1208
1274
|
};
|
|
1209
1275
|
});
|
|
1276
|
+
pendingCleanups = (file.pendingCleanups ?? []).map((intent) => ({
|
|
1277
|
+
...intent,
|
|
1278
|
+
target: { ...intent.target },
|
|
1279
|
+
}));
|
|
1210
1280
|
syncObservations = (file.syncObservations ?? []).map((observation) => ({
|
|
1211
1281
|
...observation,
|
|
1212
1282
|
target: { ...observation.target },
|
|
@@ -1269,6 +1339,10 @@ export function createTelegramTopicTargetStore(
|
|
|
1269
1339
|
...provision,
|
|
1270
1340
|
...(provision.target ? { target: { ...provision.target } } : {}),
|
|
1271
1341
|
})),
|
|
1342
|
+
pendingCleanups: pendingCleanups.map((intent) => ({
|
|
1343
|
+
...intent,
|
|
1344
|
+
target: { ...intent.target },
|
|
1345
|
+
})),
|
|
1272
1346
|
syncObservations: syncObservations.map((observation) => ({
|
|
1273
1347
|
...observation,
|
|
1274
1348
|
target: { ...observation.target },
|
|
@@ -1355,6 +1429,12 @@ export function createTelegramTopicTargetStore(
|
|
|
1355
1429
|
...(provision.target ? { target: { ...provision.target } } : {}),
|
|
1356
1430
|
}));
|
|
1357
1431
|
},
|
|
1432
|
+
listPendingCleanups() {
|
|
1433
|
+
return pendingCleanups.map((intent) => ({
|
|
1434
|
+
...intent,
|
|
1435
|
+
target: { ...intent.target },
|
|
1436
|
+
}));
|
|
1437
|
+
},
|
|
1358
1438
|
listSyncObservations() {
|
|
1359
1439
|
return syncObservations.map((observation) => ({
|
|
1360
1440
|
...observation,
|
|
@@ -1425,6 +1505,21 @@ export function createTelegramTopicTargetStore(
|
|
|
1425
1505
|
if (changed) markDirty();
|
|
1426
1506
|
return changed;
|
|
1427
1507
|
},
|
|
1508
|
+
upsertPendingCleanup(intent) {
|
|
1509
|
+
const next = { ...intent, target: { ...intent.target } };
|
|
1510
|
+
pendingCleanups = pendingCleanups.filter(
|
|
1511
|
+
(existing) => existing.id !== next.id,
|
|
1512
|
+
);
|
|
1513
|
+
pendingCleanups.push(next);
|
|
1514
|
+
markDirty();
|
|
1515
|
+
},
|
|
1516
|
+
removePendingCleanup(id) {
|
|
1517
|
+
const before = pendingCleanups.length;
|
|
1518
|
+
pendingCleanups = pendingCleanups.filter((intent) => intent.id !== id);
|
|
1519
|
+
const changed = pendingCleanups.length !== before;
|
|
1520
|
+
if (changed) markDirty();
|
|
1521
|
+
return changed;
|
|
1522
|
+
},
|
|
1428
1523
|
getBotState() {
|
|
1429
1524
|
return Object.fromEntries(
|
|
1430
1525
|
Object.entries(botState).filter(([, value]) => value !== undefined),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"test": "node --experimental-strip-types --test --test-reporter=dot tests/*.test.ts",
|
|
31
31
|
"test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
|
-
"audit": "
|
|
33
|
+
"audit": "npm audit --omit=peer",
|
|
34
|
+
"audit:host": "npm audit",
|
|
34
35
|
"pack:check": "npm pack --dry-run",
|
|
35
36
|
"validate": "npm run typecheck && npm test && npm run audit && npm run pack:check"
|
|
36
37
|
},
|
|
@@ -74,29 +75,5 @@
|
|
|
74
75
|
"devDependencies": {
|
|
75
76
|
"@types/node": "latest",
|
|
76
77
|
"typescript": "latest"
|
|
77
|
-
},
|
|
78
|
-
"overrides": {
|
|
79
|
-
"brace-expansion": "5.0.7",
|
|
80
|
-
"protobufjs": "7.6.5",
|
|
81
|
-
"undici": "8.5.0",
|
|
82
|
-
"ws": "8.21.0",
|
|
83
|
-
"@earendil-works/pi-coding-agent": {
|
|
84
|
-
"brace-expansion": "5.0.7",
|
|
85
|
-
"protobufjs": "7.6.5",
|
|
86
|
-
"undici": "8.5.0",
|
|
87
|
-
"ws": "8.21.0",
|
|
88
|
-
"@google/genai": {
|
|
89
|
-
"protobufjs": "7.6.5",
|
|
90
|
-
"ws": "8.21.0"
|
|
91
|
-
},
|
|
92
|
-
"@earendil-works/pi-ai": {
|
|
93
|
-
"protobufjs": "7.6.5",
|
|
94
|
-
"ws": "8.21.0",
|
|
95
|
-
"@google/genai": {
|
|
96
|
-
"protobufjs": "7.6.5",
|
|
97
|
-
"ws": "8.21.0"
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
78
|
}
|
|
102
79
|
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dependency audit command adapter
|
|
3
|
-
* Runs raw npm audit, prints its output, and applies the fail-closed repository policy
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
evaluateDependencyAudit,
|
|
12
|
-
type AuditReport,
|
|
13
|
-
} from "./dependency-audit-policy.ts";
|
|
14
|
-
|
|
15
|
-
function readInstalledPackageVersion(root: string, nodePath: string): string {
|
|
16
|
-
if (
|
|
17
|
-
path.isAbsolute(nodePath) ||
|
|
18
|
-
nodePath.includes("..") ||
|
|
19
|
-
!nodePath.startsWith("node_modules/")
|
|
20
|
-
) {
|
|
21
|
-
throw new Error(`unsafe installed package path: ${nodePath}`);
|
|
22
|
-
}
|
|
23
|
-
const packageJsonPath = path.join(root, nodePath, "package.json");
|
|
24
|
-
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
|
25
|
-
version?: unknown;
|
|
26
|
-
};
|
|
27
|
-
if (typeof parsed.version !== "string") {
|
|
28
|
-
throw new Error(`installed package has no valid version: ${nodePath}`);
|
|
29
|
-
}
|
|
30
|
-
return parsed.version;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function run(): void {
|
|
34
|
-
const result = spawnSync("npm", ["audit", "--json"], {
|
|
35
|
-
cwd: process.cwd(),
|
|
36
|
-
encoding: "utf8",
|
|
37
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
38
|
-
});
|
|
39
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
40
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
41
|
-
if (result.error) throw result.error;
|
|
42
|
-
if (result.signal || (result.status !== 0 && result.status !== 1)) {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`npm audit command failed: status=${String(result.status)} signal=${String(result.signal)}`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let report: AuditReport;
|
|
49
|
-
try {
|
|
50
|
-
report = JSON.parse(result.stdout) as AuditReport;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
throw new Error(`could not parse npm audit JSON: ${String(error)}`);
|
|
53
|
-
}
|
|
54
|
-
const evaluation = evaluateDependencyAudit(
|
|
55
|
-
report,
|
|
56
|
-
(nodePath) => readInstalledPackageVersion(process.cwd(), nodePath),
|
|
57
|
-
);
|
|
58
|
-
const expectedStatus = evaluation.vulnerabilityCount === 0 ? 0 : 1;
|
|
59
|
-
if (result.status !== expectedStatus) {
|
|
60
|
-
throw new Error(
|
|
61
|
-
`npm audit exit status mismatch: expected ${expectedStatus}, got ${String(result.status)}`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
if (evaluation.vulnerabilityCount === 0) {
|
|
65
|
-
console.log("Dependency audit passed with zero vulnerabilities.");
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
console.warn(
|
|
69
|
-
`Accepted ${evaluation.vulnerabilityCount} audit graph entries rooted only in approved sources ${evaluation.acceptedAdvisorySources.join(", ")}; exception expires after 2026-08-21 UTC.`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
run();
|
|
75
|
-
} catch (error) {
|
|
76
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
77
|
-
process.exitCode = 1;
|
|
78
|
-
}
|