@ctliz/agent-intercom-pi 0.11.0-connect.2

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nico Bailon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,116 @@
1
+ import { createHash } from "crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync } from "fs";
3
+ import { join } from "path";
4
+ import { ensureIntercomRuntimeDir, getIntercomDirPath, INTERCOM_DIR_MODE, restrictIntercomRuntimeFile } from "./broker/paths.ts";
5
+ import { writeDurableJson } from "./durable-json.ts";
6
+ import type { Message } from "./types.ts";
7
+
8
+ const OUTBOX_STATE_VERSION = 1;
9
+ const MAX_OUTBOX_MESSAGES = 256;
10
+
11
+ export interface StoredOutboundMessage {
12
+ to: string;
13
+ message: Message;
14
+ queuedAt: number;
15
+ }
16
+
17
+ interface OutboxState {
18
+ version: typeof OUTBOX_STATE_VERSION;
19
+ entries: StoredOutboundMessage[];
20
+ }
21
+
22
+ function fingerprint(entry: Pick<StoredOutboundMessage, "to" | "message">): string {
23
+ return JSON.stringify({
24
+ to: entry.to,
25
+ replyTo: entry.message.replyTo,
26
+ expectsReply: entry.message.expectsReply,
27
+ content: entry.message.content,
28
+ });
29
+ }
30
+
31
+ function isStoredOutboundMessage(value: unknown): value is StoredOutboundMessage {
32
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
33
+ const entry = value as Record<string, unknown>;
34
+ if (typeof entry.to !== "string" || typeof entry.queuedAt !== "number") return false;
35
+ if (typeof entry.message !== "object" || entry.message === null || Array.isArray(entry.message)) return false;
36
+ const message = entry.message as Record<string, unknown>;
37
+ return typeof message.id === "string"
38
+ && typeof message.timestamp === "number"
39
+ && typeof message.content === "object"
40
+ && message.content !== null
41
+ && typeof (message.content as Record<string, unknown>).text === "string";
42
+ }
43
+
44
+ function fileName(sessionId: string): string {
45
+ return `${createHash("sha256").update(sessionId).digest("hex")}.json`;
46
+ }
47
+
48
+ export class PersistentOutboundOutbox {
49
+ private readonly directory: string;
50
+ private readonly filePath: string;
51
+ private state: OutboxState;
52
+
53
+ constructor(sessionId: string, intercomDir: string = getIntercomDirPath()) {
54
+ ensureIntercomRuntimeDir(intercomDir);
55
+ this.directory = join(intercomDir, "outbox");
56
+ mkdirSync(this.directory, { recursive: true, mode: INTERCOM_DIR_MODE });
57
+ if (process.platform !== "win32") chmodSync(this.directory, INTERCOM_DIR_MODE);
58
+ this.filePath = join(this.directory, fileName(sessionId));
59
+ this.state = this.load();
60
+ }
61
+
62
+ list(): StoredOutboundMessage[] {
63
+ return this.state.entries.map((entry) => ({ ...entry, message: { ...entry.message, content: { ...entry.message.content } } }));
64
+ }
65
+
66
+ enqueue(to: string, message: Message): "added" | "existing" {
67
+ const existing = this.state.entries.find((entry) => entry.message.id === message.id);
68
+ if (existing) {
69
+ if (fingerprint(existing) !== fingerprint({ to, message })) {
70
+ throw new Error(`Message ID ${message.id} is already queued with a different payload`);
71
+ }
72
+ return "existing";
73
+ }
74
+ if (this.state.entries.length >= MAX_OUTBOX_MESSAGES) {
75
+ throw new Error(`Durable outbox is full (${MAX_OUTBOX_MESSAGES} messages)`);
76
+ }
77
+ this.state.entries.push({ to, message, queuedAt: Date.now() });
78
+ this.persist();
79
+ return "added";
80
+ }
81
+
82
+ remove(messageId: string): void {
83
+ const remaining = this.state.entries.filter((entry) => entry.message.id !== messageId);
84
+ if (remaining.length === this.state.entries.length) return;
85
+ this.state.entries = remaining;
86
+ this.persist();
87
+ }
88
+
89
+ clear(): void {
90
+ if (this.state.entries.length === 0) return;
91
+ this.state.entries = [];
92
+ this.persist();
93
+ }
94
+
95
+ private load(): OutboxState {
96
+ if (!existsSync(this.filePath)) return { version: OUTBOX_STATE_VERSION, entries: [] };
97
+ try {
98
+ const parsed: unknown = JSON.parse(readFileSync(this.filePath, "utf-8"));
99
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("expected object");
100
+ const state = parsed as Record<string, unknown>;
101
+ if (state.version !== OUTBOX_STATE_VERSION || !Array.isArray(state.entries) || !state.entries.every(isStoredOutboundMessage)) {
102
+ throw new Error("invalid outbox state");
103
+ }
104
+ return { version: OUTBOX_STATE_VERSION, entries: state.entries };
105
+ } catch {
106
+ const corruptPath = `${this.filePath}.corrupt-${Date.now()}`;
107
+ renameSync(this.filePath, corruptPath);
108
+ restrictIntercomRuntimeFile(corruptPath);
109
+ return { version: OUTBOX_STATE_VERSION, entries: [] };
110
+ }
111
+ }
112
+
113
+ private persist(): void {
114
+ writeDurableJson(this.filePath, this.state);
115
+ }
116
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@ctliz/agent-intercom-pi",
3
+ "version": "0.11.0-connect.2",
4
+ "license": "AGPL-3.0-or-later",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "banner.png",
12
+ "*.ts",
13
+ "!*.test.ts",
14
+ "broker/**/*.ts",
15
+ "!broker/**/*.test.ts",
16
+ "protocol-v4/**/*.ts",
17
+ "!protocol-v4/**/*.test.ts",
18
+ "provider/provider.mjs",
19
+ "!provider/entry.ts",
20
+ "ui/**/*.ts",
21
+ "skills/**/*",
22
+ "!**/*.test.ts",
23
+ "THIRD_PARTY_NOTICES.md",
24
+ "licenses/**/*",
25
+ "COPYRIGHT",
26
+ "LICENSE_TRANSITION.md"
27
+ ],
28
+ "scripts": {
29
+ "build:protected-provider": "node scripts/build-protected-provider.mjs",
30
+ "prepack": "npm run build:protected-provider",
31
+ "test": "tsx --test broker/*.test.ts *.test.ts test/*.test.ts protocol-v4/*.test.ts",
32
+ "test:ci": "tsx --test --test-concurrency=1 broker/*.test.ts *.test.ts test/*.test.ts protocol-v4/*.test.ts",
33
+ "typecheck": "tsc --noEmit"
34
+ },
35
+ "keywords": [
36
+ "pi-package"
37
+ ],
38
+ "pi": {
39
+ "extensions": [
40
+ "./index.ts"
41
+ ],
42
+ "skills": [
43
+ "./skills"
44
+ ]
45
+ },
46
+ "dependencies": {
47
+ "@ctliz/agent-intercom-core": "git+https://github.com/ctliz/agent-intercom-core.git#37e074970e2a9de32a16fc325607c3b476b0bd45",
48
+ "@earendil-works/pi-ai": "*",
49
+ "@earendil-works/pi-coding-agent": "*",
50
+ "@earendil-works/pi-tui": "*",
51
+ "tsx": "^4.20.0",
52
+ "typebox": "*"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.15.0",
56
+ "typescript": "^5.9.3"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "engines": {
62
+ "node": ">=22.19.0"
63
+ },
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/ctliz/agent-intercom-pi.git"
67
+ },
68
+ "homepage": "https://github.com/ctliz/agent-intercom-pi#readme",
69
+ "bugs": {
70
+ "url": "https://github.com/ctliz/agent-intercom-pi/issues"
71
+ }
72
+ }
@@ -0,0 +1,39 @@
1
+ import {
2
+ INTERCOM_PROTOCOL_NAME,
3
+ INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
4
+ INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
5
+ INTERCOM_PROTOCOL_V4_VECTORS,
6
+ INTERCOM_PROTOCOL_VERSION,
7
+ INTERCOM_SCOPE_ENV,
8
+ INTERCOM_SCOPE_ID_PATTERN,
9
+ INTERCOM_SCOPE_ID_PATTERN_SOURCE,
10
+ intercomScopeIdFromEnv,
11
+ parseIntercomScopeId,
12
+ sameIntercomScope,
13
+ } from "@ctliz/agent-intercom-core/protocol-v4";
14
+
15
+ export const INTERCOM_REGISTRATION_METADATA_ERROR = "Registration metadata is invalid" as const;
16
+
17
+ export function parseIntercomScopeIdForRegistration(value: unknown): string | undefined {
18
+ try { return parseIntercomScopeId(value, "registrationMetadata"); }
19
+ catch { throw new Error(INTERCOM_REGISTRATION_METADATA_ERROR); }
20
+ }
21
+
22
+ export function intercomScopeIdFromEnvForRegistration(env: NodeJS.ProcessEnv = process.env): string | undefined {
23
+ try { return intercomScopeIdFromEnv(env); }
24
+ catch { throw new Error(INTERCOM_REGISTRATION_METADATA_ERROR); }
25
+ }
26
+
27
+ export {
28
+ INTERCOM_PROTOCOL_NAME,
29
+ INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
30
+ INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
31
+ INTERCOM_PROTOCOL_V4_VECTORS,
32
+ INTERCOM_PROTOCOL_VERSION,
33
+ INTERCOM_SCOPE_ENV,
34
+ INTERCOM_SCOPE_ID_PATTERN,
35
+ INTERCOM_SCOPE_ID_PATTERN_SOURCE,
36
+ intercomScopeIdFromEnv,
37
+ parseIntercomScopeId,
38
+ sameIntercomScope,
39
+ };
@@ -0,0 +1,34 @@
1
+ // Generated from provider/entry.ts by scripts/build-protected-provider.mjs.
2
+ // Do not edit this artifact directly.
3
+ const supportedBossFeature = Object.freeze({
4
+ version: "intercom.broker-feature.v1",
5
+ feature: "boss-run-v1",
6
+ featureVersion: 1,
7
+ semanticsHash: "8943eb60d29afa5264322b5cc7df3de245b01b1cf48a8cbf9cfb6188b02fcfa9",
8
+ controlEnvelopeVersion: 1,
9
+ capabilityDigest: "239bee8bb64cc8c149d49ac00c7396f33f375a89a1d6dea6bd86ff840d551a59",
10
+ });
11
+ /** Immutable build-time contract only; it is not an installed-provider claim. */
12
+ export const PI_BOSS_PROTECTED_PROVIDER_BUILD_IDENTITY = Object.freeze({
13
+ contractVersion: "pi.boss-protected-provider.v1",
14
+ adapterId: "pi",
15
+ providerPackage: "@ctliz/agent-intercom-pi",
16
+ supportedBaseProtocolVersions: Object.freeze([4]),
17
+ supportedFeatures: Object.freeze([supportedBossFeature]),
18
+ protocolFeatureContractHash: "dae30efe2c48d2de0fe72a7ebdfd107d3feaefc180d42056ba05df6088a94364",
19
+ authoritative: false,
20
+ providerStartAvailable: false,
21
+ bossAdvertisementEnabled: false,
22
+ });
23
+ const BOSS_PROTECTED_PROVIDER_START_UNAVAILABLE = "BOSS_PROTECTED_PROVIDER_START_UNAVAILABLE";
24
+ class PiBossProtectedProviderStartUnavailableError extends Error {
25
+ code = BOSS_PROTECTED_PROVIDER_START_UNAVAILABLE;
26
+ constructor() {
27
+ super(`${BOSS_PROTECTED_PROVIDER_START_UNAVAILABLE}: protected provider execution is not installed`);
28
+ this.name = "PiBossProtectedProviderStartUnavailableError";
29
+ }
30
+ }
31
+ /** A later protected service may replace this dormant entry after release gates. */
32
+ export function startPiBossProtectedProvider(_request) {
33
+ throw new PiBossProtectedProviderStartUnavailableError();
34
+ }
@@ -0,0 +1,231 @@
1
+ import { createHash } from "node:crypto";
2
+ import { getAskTimeoutMs } from "./config.ts";
3
+ import type { Message, SessionInfo } from "./types.ts";
4
+
5
+ export interface IntercomContext {
6
+ from: SessionInfo;
7
+ message: Message;
8
+ receivedAt: number;
9
+ deferredAt?: number;
10
+ }
11
+
12
+ export type ReplyWhich = "oldest" | "latest";
13
+
14
+ /** Stable, receiver-local selector that does not expose the wire message ID. */
15
+ export function pendingAskId(fromSessionId: string, messageId: string): string {
16
+ const digest = createHash("sha256")
17
+ .update(fromSessionId)
18
+ .update("\0")
19
+ .update(messageId)
20
+ .digest("base64url");
21
+ return `ask-${digest}`;
22
+ }
23
+
24
+ function matchesPendingSender(context: IntercomContext, to: string): boolean {
25
+ if (context.from.id === to) {
26
+ return true;
27
+ }
28
+
29
+ return context.from.name?.toLowerCase() === to.toLowerCase();
30
+ }
31
+
32
+ function contextKey(fromSessionId: string, messageId: string): string {
33
+ return `${fromSessionId}\u0000${messageId}`;
34
+ }
35
+
36
+ function selectByAge(contexts: IntercomContext[], which: ReplyWhich): IntercomContext {
37
+ const sorted = [...contexts].sort((a, b) => a.receivedAt - b.receivedAt);
38
+ return which === "oldest" ? sorted[0]! : sorted[sorted.length - 1]!;
39
+ }
40
+
41
+ function distinctSenders(contexts: IntercomContext[]): number {
42
+ return new Set(contexts.map((context) => context.from.id)).size;
43
+ }
44
+
45
+ export class ReplyTracker {
46
+ private readonly pendingAsks = new Map<string, IntercomContext>();
47
+ private readonly pendingTurnContexts: IntercomContext[][] = [];
48
+ private currentTurnContexts: IntercomContext[] = [];
49
+
50
+ constructor(private readonly askTimeoutMs = getAskTimeoutMs()) {}
51
+
52
+ recordIncomingMessage(from: SessionInfo, message: Message, receivedAt = Date.now()): IntercomContext {
53
+ const key = contextKey(from.id, message.id);
54
+ const existing = this.pendingAsks.get(key);
55
+ const context = {
56
+ from,
57
+ message,
58
+ receivedAt,
59
+ ...(existing?.deferredAt === undefined ? {} : { deferredAt: existing.deferredAt }),
60
+ };
61
+ if (message.expectsReply) {
62
+ this.pendingAsks.set(key, context);
63
+ }
64
+ return context;
65
+ }
66
+
67
+ queueTurnContext(context: IntercomContext): void {
68
+ this.queueTurnContexts([context]);
69
+ }
70
+
71
+ queueTurnContexts(contexts: readonly IntercomContext[]): void {
72
+ if (contexts.length > 0) {
73
+ this.pendingTurnContexts.push([...contexts]);
74
+ }
75
+ }
76
+
77
+ beginTurn(now = Date.now()): void {
78
+ this.pruneExpired(now);
79
+ if (this.currentTurnContexts.length === 0) {
80
+ this.currentTurnContexts = this.pendingTurnContexts.shift() ?? [];
81
+ }
82
+ }
83
+
84
+ endTurn(): void {
85
+ this.currentTurnContexts = [];
86
+ }
87
+
88
+ reset(): void {
89
+ this.pendingAsks.clear();
90
+ this.pendingTurnContexts.length = 0;
91
+ this.currentTurnContexts = [];
92
+ }
93
+
94
+ resolveReplyTarget(options: { to?: string; replyTo?: string; askId?: string; which?: ReplyWhich }, now = Date.now()): IntercomContext {
95
+ this.pruneExpired(now);
96
+
97
+ if (options.askId) {
98
+ const match = Array.from(this.pendingAsks.values()).find((context) =>
99
+ pendingAskId(context.from.id, context.message.id) === options.askId
100
+ );
101
+ if (!match) {
102
+ throw new Error(`No pending ask with ask ID "${options.askId}"`);
103
+ }
104
+ if (options.to && !matchesPendingSender(match, options.to)) {
105
+ throw new Error(`Pending ask "${options.askId}" is not from "${options.to}"`);
106
+ }
107
+ return match;
108
+ }
109
+
110
+ if (options.replyTo) {
111
+ const candidates = Array.from(this.pendingAsks.values()).filter((context) => context.message.id === options.replyTo);
112
+ if (candidates.length === 0) {
113
+ throw new Error(`No pending ask with message ID "${options.replyTo}"`);
114
+ }
115
+ const matches = options.to ? candidates.filter((context) => matchesPendingSender(context, options.to!)) : candidates;
116
+ if (matches.length === 0) {
117
+ throw new Error(`Pending ask "${options.replyTo}" is not from "${options.to}"`);
118
+ }
119
+ if (matches.length > 1) {
120
+ throw new Error(`Multiple pending asks use message ID "${options.replyTo}" — specify \`to\``);
121
+ }
122
+ return matches[0]!;
123
+ }
124
+
125
+ if (this.currentTurnContexts.length > 0) {
126
+ const turnMatches = options.to
127
+ ? this.currentTurnContexts.filter((context) => matchesPendingSender(context, options.to!))
128
+ : this.currentTurnContexts;
129
+ const replyableMatches = turnMatches.filter((context) => context.message.expectsReply);
130
+ if (replyableMatches.length === 1) {
131
+ return replyableMatches[0]!;
132
+ }
133
+ if (replyableMatches.length > 1) {
134
+ if (!options.to && distinctSenders(replyableMatches) > 1) {
135
+ throw new Error("Multiple asks are active in this intercom batch — specify `to` to select the sender");
136
+ }
137
+ if (options.which) return selectByAge(replyableMatches, options.which);
138
+ throw new Error("Multiple asks from the selected sender are active — specify `which` as `oldest` or `latest`");
139
+ }
140
+ if (turnMatches.length === 1) {
141
+ return turnMatches[0]!;
142
+ }
143
+ if (turnMatches.length > 1) {
144
+ if (distinctSenders(turnMatches) === 1) return selectByAge(turnMatches, "latest");
145
+ throw new Error("Multiple senders are active in this intercom batch — specify `to` using an exact sender name or full session ID");
146
+ }
147
+ }
148
+
149
+ const pending = Array.from(this.pendingAsks.values());
150
+ if (pending.length === 1) {
151
+ return pending[0]!;
152
+ }
153
+
154
+ const matches = options.to
155
+ ? pending.filter((context) => matchesPendingSender(context, options.to!))
156
+ : pending;
157
+ if (matches.length === 1) return matches[0]!;
158
+ if (matches.length > 1) {
159
+ if (!options.to && distinctSenders(matches) > 1) {
160
+ throw new Error("Multiple pending asks — specify `to` using a sender from `intercom_pending`");
161
+ }
162
+ if (options.which) return selectByAge(matches, options.which);
163
+ const sender = options.to ? ` from \"${options.to}\"` : "";
164
+ throw new Error(`Multiple pending asks${sender} — specify \`which\` as \`oldest\` or \`latest\``);
165
+ }
166
+ if (pending.length === 0) {
167
+ throw new Error("No active intercom context to reply to");
168
+ }
169
+ if (options.to) throw new Error(`No pending ask from \"${options.to}\"`);
170
+ throw new Error("No matching pending ask");
171
+ }
172
+
173
+ markReplied(replyTo: string, fromSessionId?: string): void {
174
+ this.dismissPendingAsk(replyTo, fromSessionId);
175
+ }
176
+
177
+ dismissOrdinarySender(fromSessionId: string): void {
178
+ this.currentTurnContexts = this.currentTurnContexts.filter((context) =>
179
+ context.message.expectsReply || context.from.id !== fromSessionId
180
+ );
181
+ }
182
+
183
+ markDeferred(replyTo: string, fromSessionIdOrDeferredAt?: string | number, deferredAt = Date.now()): boolean {
184
+ const fromSessionId = typeof fromSessionIdOrDeferredAt === "string" ? fromSessionIdOrDeferredAt : undefined;
185
+ const effectiveDeferredAt = typeof fromSessionIdOrDeferredAt === "number" ? fromSessionIdOrDeferredAt : deferredAt;
186
+ let changed = false;
187
+ for (const context of this.pendingAsks.values()) {
188
+ if (context.message.id === replyTo && (!fromSessionId || context.from.id === fromSessionId)) {
189
+ context.deferredAt = effectiveDeferredAt;
190
+ changed = true;
191
+ }
192
+ }
193
+ return changed;
194
+ }
195
+
196
+ dismissPendingAsk(replyTo: string, fromSessionId?: string): void {
197
+ for (const [key, context] of this.pendingAsks) {
198
+ if (context.message.id === replyTo && (!fromSessionId || context.from.id === fromSessionId)) {
199
+ this.pendingAsks.delete(key);
200
+ }
201
+ }
202
+ for (let batchIndex = this.pendingTurnContexts.length - 1; batchIndex >= 0; batchIndex -= 1) {
203
+ const batch = this.pendingTurnContexts[batchIndex]!;
204
+ for (let contextIndex = batch.length - 1; contextIndex >= 0; contextIndex -= 1) {
205
+ if (
206
+ batch[contextIndex]?.message.id === replyTo
207
+ && (!fromSessionId || batch[contextIndex]?.from.id === fromSessionId)
208
+ ) {
209
+ batch.splice(contextIndex, 1);
210
+ }
211
+ }
212
+ if (batch.length === 0) this.pendingTurnContexts.splice(batchIndex, 1);
213
+ }
214
+ this.currentTurnContexts = this.currentTurnContexts.filter((context) =>
215
+ context.message.id !== replyTo || (fromSessionId !== undefined && context.from.id !== fromSessionId)
216
+ );
217
+ }
218
+
219
+ listPending(now = Date.now()): IntercomContext[] {
220
+ this.pruneExpired(now);
221
+ return Array.from(this.pendingAsks.values()).sort((a, b) => a.receivedAt - b.receivedAt);
222
+ }
223
+
224
+ private pruneExpired(now: number): void {
225
+ for (const context of Array.from(this.pendingAsks.values())) {
226
+ if (now - context.receivedAt > this.askTimeoutMs) {
227
+ this.dismissPendingAsk(context.message.id, context.from.id);
228
+ }
229
+ }
230
+ }
231
+ }