toolroll 0.9.16 → 0.9.18
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 +28 -0
- package/dist/assignment-brief.js +11 -4
- package/dist/browser/workspace.js +14 -14
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/chat-actions.d.ts +7 -0
- package/dist/chat-actions.js +40 -4
- package/dist/chat-ask.js +29 -15
- package/dist/chat-channel.js +3 -1
- package/dist/chat-decide.d.ts +203 -0
- package/dist/chat-decide.js +554 -0
- package/dist/chat-delivery-state.d.ts +7 -0
- package/dist/chat-delivery.d.ts +3 -0
- package/dist/chat-delivery.js +63 -17
- package/dist/chat-quiet.d.ts +4 -1
- package/dist/chat-quiet.js +6 -2
- package/dist/chat-warmth.d.ts +21 -0
- package/dist/chat-warmth.js +36 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-api.d.ts +3 -1
- package/dist/discord-api.js +4 -2
- package/dist/discord-chat.d.ts +3 -2
- package/dist/discord-chat.js +35 -6
- package/dist/lead-about.d.ts +35 -0
- package/dist/lead-about.js +62 -0
- package/dist/lead-capabilities.d.ts +64 -0
- package/dist/lead-capabilities.js +199 -0
- package/dist/lead-commitments.js +13 -7
- package/dist/lead-context.d.ts +5 -3
- package/dist/lead-context.js +29 -7
- package/dist/lead-follow.d.ts +1 -1
- package/dist/lead-follow.js +3 -2
- package/dist/lead-identity.js +1 -1
- package/dist/lead-people.d.ts +55 -0
- package/dist/lead-people.js +128 -0
- package/dist/lead-voice.d.ts +3 -2
- package/dist/lead-voice.js +4 -3
- package/dist/mate-cli.d.ts +3 -0
- package/dist/mate-cli.js +15 -14
- package/dist/mate-contract.d.ts +1 -1
- package/dist/mate-contract.js +6 -4
- package/dist/mate-progress.js +2 -0
- package/dist/mate-tools.d.ts +8 -0
- package/dist/mate-tools.js +112 -6
- package/dist/mate.d.ts +26 -1
- package/dist/mate.js +89 -25
- package/dist/modes.d.ts +6 -0
- package/dist/modes.js +6 -0
- package/dist/operate.js +20 -8
- package/dist/phase-routing.d.ts +1 -1
- package/dist/project-delete.js +3 -0
- package/dist/provider-auth.d.ts +4 -0
- package/dist/provider-auth.js +1 -1
- package/dist/reply-shape.d.ts +96 -0
- package/dist/reply-shape.js +452 -0
- package/dist/result-shots.d.ts +51 -0
- package/dist/result-shots.js +119 -0
- package/dist/serve.js +76 -49
- package/dist/slack-chat.d.ts +2 -0
- package/dist/slack-chat.js +38 -7
- package/dist/store.d.ts +36 -3
- package/dist/store.js +163 -16
- package/dist/team-runtime.js +1 -1
- package/dist/teams-chat.js +17 -1
- package/dist/telegram-mate.d.ts +12 -0
- package/dist/telegram-mate.js +27 -11
- package/dist/telegram-team.js +6 -3
- package/dist/telegram.d.ts +5 -3
- package/dist/telegram.js +227 -21
- package/dist/test-temp.js +1 -1
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -148,7 +148,8 @@ CREATE TABLE IF NOT EXISTS notification_preference (
|
|
|
148
148
|
digest_at TEXT CHECK (digest_at IS NULL OR (length(digest_at) = 5 AND digest_at GLOB '[0-2][0-9]:[0-5][0-9]')),
|
|
149
149
|
digest_on TEXT,
|
|
150
150
|
updated_at TEXT NOT NULL,
|
|
151
|
-
updated_by TEXT NOT NULL
|
|
151
|
+
updated_by TEXT NOT NULL,
|
|
152
|
+
screenshots TEXT NOT NULL DEFAULT 'off' CHECK (screenshots IN ('off', 'first', 'all'))
|
|
152
153
|
);
|
|
153
154
|
CREATE TABLE IF NOT EXISTS chat_card (
|
|
154
155
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -183,14 +184,28 @@ CREATE TABLE IF NOT EXISTS chat_batch_item (
|
|
|
183
184
|
PRIMARY KEY (batch, task_ref)
|
|
184
185
|
);
|
|
185
186
|
`;
|
|
187
|
+
/** Screenshots with results (no version bump: additive only): each saved screenshot a destination was sent with a
|
|
188
|
+
* result, marked before the upload so a retry never sends it twice. No foreign keys: a deleted project's runs take
|
|
189
|
+
* their rows with them (project-delete.ts). */
|
|
190
|
+
const RESULT_SHOT_SCHEMA = `
|
|
191
|
+
CREATE TABLE IF NOT EXISTS result_shot_sent (
|
|
192
|
+
destination TEXT NOT NULL,
|
|
193
|
+
run INTEGER NOT NULL,
|
|
194
|
+
artifact INTEGER NOT NULL,
|
|
195
|
+
sent_at TEXT NOT NULL,
|
|
196
|
+
PRIMARY KEY (destination, run, artifact)
|
|
197
|
+
);
|
|
198
|
+
`;
|
|
186
199
|
/** Settings → Lead (no version bump: additive only): what one person calls their lead and the short persona it
|
|
187
|
-
* speaks with
|
|
200
|
+
* speaks with, and what their lead knows about them (`about_json`: the confirmed lines, lead-about.ts). No row reads
|
|
201
|
+
* as the defaults (lead-identity.ts). */
|
|
188
202
|
const LEAD_CONFIG_SCHEMA = `
|
|
189
203
|
CREATE TABLE IF NOT EXISTS lead_config (
|
|
190
204
|
account TEXT PRIMARY KEY,
|
|
191
205
|
name TEXT NOT NULL,
|
|
192
206
|
persona TEXT NOT NULL,
|
|
193
|
-
updated_at TEXT NOT NULL
|
|
207
|
+
updated_at TEXT NOT NULL,
|
|
208
|
+
about_json TEXT NOT NULL DEFAULT '[]'
|
|
194
209
|
);
|
|
195
210
|
`;
|
|
196
211
|
/** Pings follow responsibility (no version bump: additive only). `lead_credential` is a lead token: an agent acting
|
|
@@ -293,9 +308,45 @@ CREATE TABLE IF NOT EXISTS mate_ask (
|
|
|
293
308
|
);
|
|
294
309
|
CREATE INDEX IF NOT EXISTS mate_ask_thread ON mate_ask (thread, turn);
|
|
295
310
|
`;
|
|
311
|
+
/** Decisions in the chat app (chat-decide.ts; no version bump: additive only). A token names one act on one card (the chat and message it rides); `digest` is
|
|
312
|
+
* what the card showed — a receipt, a revision source, a task stamp, a plan or a commit. */
|
|
313
|
+
export const CHAT_DECIDE_SCHEMA = `
|
|
314
|
+
CREATE TABLE IF NOT EXISTS chat_decide_action (
|
|
315
|
+
token TEXT PRIMARY KEY,
|
|
316
|
+
channel TEXT NOT NULL,
|
|
317
|
+
binding INTEGER NOT NULL,
|
|
318
|
+
chat TEXT NOT NULL,
|
|
319
|
+
message TEXT,
|
|
320
|
+
act TEXT NOT NULL CHECK (act IN ('accept','changes','retry','approve','not-now','merge')),
|
|
321
|
+
phase TEXT NOT NULL CHECK (phase IN ('offer','yes','cancel')),
|
|
322
|
+
task_id TEXT NOT NULL,
|
|
323
|
+
run INTEGER,
|
|
324
|
+
digest TEXT NOT NULL,
|
|
325
|
+
created_at TEXT NOT NULL,
|
|
326
|
+
expires_at TEXT NOT NULL,
|
|
327
|
+
consumed_at TEXT
|
|
328
|
+
);
|
|
329
|
+
CREATE INDEX IF NOT EXISTS chat_decide_action_message ON chat_decide_action (channel, chat, message);
|
|
330
|
+
CREATE TABLE IF NOT EXISTS chat_decide_prompt (
|
|
331
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
332
|
+
channel TEXT NOT NULL,
|
|
333
|
+
binding INTEGER NOT NULL,
|
|
334
|
+
chat TEXT NOT NULL,
|
|
335
|
+
task_id TEXT NOT NULL,
|
|
336
|
+
run INTEGER NOT NULL,
|
|
337
|
+
digest TEXT NOT NULL,
|
|
338
|
+
message TEXT,
|
|
339
|
+
created_at TEXT NOT NULL,
|
|
340
|
+
expires_at TEXT NOT NULL,
|
|
341
|
+
consumed_at TEXT
|
|
342
|
+
);
|
|
343
|
+
CREATE INDEX IF NOT EXISTS chat_decide_prompt_open ON chat_decide_prompt (channel, binding, consumed_at);
|
|
344
|
+
`;
|
|
296
345
|
export const MATE_ASK_OTHER = "Something else";
|
|
297
346
|
/** How long the question's buttons work, on every channel. */
|
|
298
347
|
export const MATE_ASK_TTL_MS = 7 * 86_400_000;
|
|
348
|
+
/** What a turn the lead starts itself for automatic crew updates says in the owner's place (lead-follow.ts): never their answer. */
|
|
349
|
+
export const LEAD_FOLLOW_MESSAGE = "Automatic crew update: inspect the saved results or decisions and tell me what needs attention. Do not rerun work.";
|
|
299
350
|
/** Settings → Integrations (no version bump): each integration's last check. `outcome` is ok, failed, or absent (the check found
|
|
300
351
|
* nothing set up, such as no `gh` sign-in). `ok_at` and `error`/`error_at` keep the last success and the last
|
|
301
352
|
* failure across checks, so a row can say both. `account` is a display name only, never a credential. */
|
|
@@ -767,6 +818,9 @@ const LIFECYCLE_HOLD_WORDS = {
|
|
|
767
818
|
revision: "a plan revision awaiting approval",
|
|
768
819
|
stop: "a stopped attempt that has not been resumed",
|
|
769
820
|
};
|
|
821
|
+
export const RESULT_SCREENSHOTS = ["off", "first", "all"];
|
|
822
|
+
/** One person's saved screenshots following a result message: its own outbox row, addressed to them (result-shots.ts). */
|
|
823
|
+
export const RESULT_SHOTS_KIND = "result-screenshots";
|
|
770
824
|
/** Tasks filed this close together share one chat message. */
|
|
771
825
|
export const QUIET_GROUP_MS = 60_000;
|
|
772
826
|
export function isDigestTime(value) {
|
|
@@ -3882,6 +3936,8 @@ function initializeStore(db, file) {
|
|
|
3882
3936
|
db.exec(MONITORING_SCHEMA);
|
|
3883
3937
|
db.exec(INTEGRATIONS_SCHEMA);
|
|
3884
3938
|
db.exec(MATE_ASK_SCHEMA);
|
|
3939
|
+
// Decisions in the chat app (no version bump: additive only): result, plan and merge buttons, and Request changes.
|
|
3940
|
+
db.exec(CHAT_DECIDE_SCHEMA);
|
|
3885
3941
|
db.exec(SPEND_SCHEMA);
|
|
3886
3942
|
// Sign-in pauses: one row per incident of a provider's sign-in no longer working.
|
|
3887
3943
|
db.exec(PROVIDER_AUTH_SCHEMA);
|
|
@@ -3897,10 +3953,15 @@ function initializeStore(db, file) {
|
|
|
3897
3953
|
db.exec(POLICY_SCHEMA);
|
|
3898
3954
|
db.exec(RUN_CHECK_SCHEMA);
|
|
3899
3955
|
db.exec(QUIET_CHAT_SCHEMA);
|
|
3956
|
+
// Screenshots with results: each person's choice (an older notification_preference gains it), and what was sent.
|
|
3957
|
+
addColumn(db, "notification_preference", "screenshots", "TEXT NOT NULL DEFAULT 'off' CHECK (screenshots IN ('off', 'first', 'all'))");
|
|
3958
|
+
db.exec(RESULT_SHOT_SCHEMA);
|
|
3900
3959
|
// The update a batch line speaks for: its newest fact about that task (null: a finished result).
|
|
3901
3960
|
addColumn(db, "chat_batch_item", "notification", "INTEGER");
|
|
3902
3961
|
db.exec(LEAD_QUIET_SCHEMA);
|
|
3903
3962
|
db.exec(LEAD_CONFIG_SCHEMA);
|
|
3963
|
+
// What a person's lead knows about them, beside its name and persona (an older lead_config gains it).
|
|
3964
|
+
addColumn(db, "lead_config", "about_json", "TEXT NOT NULL DEFAULT '[]'");
|
|
3904
3965
|
db.exec(LEAD_COMMITMENT_SCHEMA);
|
|
3905
3966
|
mergeCheckTables(db);
|
|
3906
3967
|
addColumn(db, "monitoring_status", "target", "TEXT");
|
|
@@ -13561,6 +13622,7 @@ export class Store {
|
|
|
13561
13622
|
reason: String(row["reason"]),
|
|
13562
13623
|
runner: String(row["runner"]),
|
|
13563
13624
|
observedAt: String(row["observed_at"]),
|
|
13625
|
+
probe: String(row["probe"]),
|
|
13564
13626
|
};
|
|
13565
13627
|
};
|
|
13566
13628
|
}
|
|
@@ -15137,6 +15199,11 @@ export class Store {
|
|
|
15137
15199
|
if (Number(changes) > 0 && actor !== null) {
|
|
15138
15200
|
this.db.prepare("INSERT OR IGNORE INTO notification_actor (notification, account, lead) VALUES (?, ?, ?)").run(Number(lastInsertRowid), actor.account, actor.lead ? 1 : 0);
|
|
15139
15201
|
}
|
|
15202
|
+
if (Number(changes) > 0 && run !== null && notification.recipient == null) {
|
|
15203
|
+
const made = this.notificationById(Number(lastInsertRowid));
|
|
15204
|
+
if (made !== null)
|
|
15205
|
+
this.enqueueResultShots(made, now);
|
|
15206
|
+
}
|
|
15140
15207
|
return Number(changes) > 0;
|
|
15141
15208
|
}
|
|
15142
15209
|
/**
|
|
@@ -19709,8 +19776,9 @@ export class Store {
|
|
|
19709
19776
|
const options = JSON.parse(String(row["options_json"]));
|
|
19710
19777
|
return { turn: Number(row["turn"]), thread: Number(row["thread"]), question: String(row["question"]), options: Array.isArray(options) ? options.map(String) : [], createdAt: String(row["created_at"]) };
|
|
19711
19778
|
}
|
|
19712
|
-
/** What a tap on the question finds: open; answered (
|
|
19713
|
-
*
|
|
19779
|
+
/** What a tap on the question finds: open; answered (its owner has written in its thread since the reply that asked
|
|
19780
|
+
* it, typed or tapped; a turn the lead started itself, or another person's message, is no answer); or expired (older
|
|
19781
|
+
* than MATE_ASK_TTL_MS, or the question or the reply that asked it is gone, as after a retention purge). */
|
|
19714
19782
|
mateAskState(turn, now) {
|
|
19715
19783
|
const ask = this.mateAsk(turn);
|
|
19716
19784
|
if (ask === null)
|
|
@@ -19718,7 +19786,10 @@ export class Store {
|
|
|
19718
19786
|
const asking = this.db.prepare("SELECT MAX(id) AS id FROM mate_message WHERE thread = ? AND turn = ? AND role = 'assistant'").get(ask.thread, turn);
|
|
19719
19787
|
if (asking === undefined || asking["id"] === null)
|
|
19720
19788
|
return { state: "expired" };
|
|
19721
|
-
const
|
|
19789
|
+
const owner = String(this.getMateTurn(turn).approver);
|
|
19790
|
+
const later = this.db.prepare(`SELECT 1 AS hit FROM mate_message m LEFT JOIN mate_turn t ON t.id = m.turn LEFT JOIN team_message tm ON tm.message = m.id
|
|
19791
|
+
WHERE m.thread = ? AND m.role = 'operator' AND m.id > ? AND m.text <> ? AND COALESCE(tm.author, t.approver, ?) = ? LIMIT 1`)
|
|
19792
|
+
.get(ask.thread, Number(asking["id"]), LEAD_FOLLOW_MESSAGE, owner, owner);
|
|
19722
19793
|
if (later !== undefined)
|
|
19723
19794
|
return { state: "answered" };
|
|
19724
19795
|
if (new Date(ask.createdAt).getTime() + MATE_ASK_TTL_MS <= now.getTime())
|
|
@@ -22217,23 +22288,69 @@ export class Store {
|
|
|
22217
22288
|
// ---- quiet chat: each person's notification choice and each task's one message ----
|
|
22218
22289
|
/** How chats reach this person: only when they are needed unless they chose every step. */
|
|
22219
22290
|
notificationPreference(account) {
|
|
22220
|
-
const row = this.db.prepare("SELECT mode, digest_at, digest_on, updated_at FROM notification_preference WHERE account = ?").get(account);
|
|
22221
|
-
return row === undefined ? { mode: "quiet", digestAt: null, digestOn: null, updatedAt: null }
|
|
22291
|
+
const row = this.db.prepare("SELECT mode, digest_at, digest_on, updated_at, screenshots FROM notification_preference WHERE account = ?").get(account);
|
|
22292
|
+
return row === undefined ? { mode: "quiet", digestAt: null, digestOn: null, updatedAt: null, screenshots: "off" }
|
|
22222
22293
|
: { mode: row["mode"] === "all" ? "all" : "quiet", digestAt: row["digest_at"] == null ? null : String(row["digest_at"]),
|
|
22223
|
-
digestOn: row["digest_on"] == null ? null : String(row["digest_on"]), updatedAt: String(row["updated_at"])
|
|
22294
|
+
digestOn: row["digest_on"] == null ? null : String(row["digest_on"]), updatedAt: String(row["updated_at"]),
|
|
22295
|
+
screenshots: row["screenshots"] === "first" || row["screenshots"] === "all" ? row["screenshots"] : "off" };
|
|
22224
22296
|
}
|
|
22225
22297
|
/** Change one person's choice; a digest time is a local HH:MM, or null for no evening digest. */
|
|
22226
22298
|
setNotificationPreference(account, change, by, now) {
|
|
22227
22299
|
if (change.digestAt != null && !isDigestTime(change.digestAt))
|
|
22228
22300
|
throw new Error("digest time must be HH:MM");
|
|
22301
|
+
if (change.screenshots !== undefined && !RESULT_SCREENSHOTS.includes(change.screenshots))
|
|
22302
|
+
throw new Error("screenshots must be off, first or all");
|
|
22229
22303
|
const current = this.notificationPreference(account);
|
|
22230
22304
|
const mode = change.mode ?? current.mode;
|
|
22231
22305
|
const digestAt = change.digestAt === undefined ? current.digestAt : change.digestAt;
|
|
22232
|
-
|
|
22233
|
-
|
|
22234
|
-
|
|
22306
|
+
const screenshots = change.screenshots ?? current.screenshots;
|
|
22307
|
+
this.db.prepare(`INSERT INTO notification_preference (account, mode, digest_at, digest_on, updated_at, updated_by, screenshots) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
22308
|
+
ON CONFLICT (account) DO UPDATE SET mode = excluded.mode, digest_at = excluded.digest_at, updated_at = excluded.updated_at, updated_by = excluded.updated_by,
|
|
22309
|
+
screenshots = excluded.screenshots`)
|
|
22310
|
+
.run(account, mode, digestAt, current.digestOn, now.toISOString(), by, screenshots);
|
|
22235
22311
|
return this.notificationPreference(account);
|
|
22236
22312
|
}
|
|
22313
|
+
// ---- screenshots with results (result-shots.ts) ----
|
|
22314
|
+
/** The screenshots of this result already sent (or being sent) to one destination. */
|
|
22315
|
+
resultShotsSent(destination, run) {
|
|
22316
|
+
return new Set(this.db.prepare("SELECT artifact FROM result_shot_sent WHERE destination = ? AND run = ?").all(destination, run).map(row => Number(row["artifact"])));
|
|
22317
|
+
}
|
|
22318
|
+
/** Mark screenshots as sent BEFORE they go: an unconfirmed send is never repeated. */
|
|
22319
|
+
markResultShotsSent(destination, run, artifacts, now) {
|
|
22320
|
+
const insert = this.db.prepare("INSERT OR IGNORE INTO result_shot_sent (destination, run, artifact, sent_at) VALUES (?, ?, ?, ?)");
|
|
22321
|
+
this.transact(() => { for (const artifact of artifacts)
|
|
22322
|
+
insert.run(destination, run, artifact, now.toISOString()); });
|
|
22323
|
+
}
|
|
22324
|
+
/** Telegram refused them outright, so nothing arrived: they may be tried again. */
|
|
22325
|
+
unmarkResultShotsSent(destination, run, artifacts) {
|
|
22326
|
+
const remove = this.db.prepare("DELETE FROM result_shot_sent WHERE destination = ? AND run = ? AND artifact = ?");
|
|
22327
|
+
this.transact(() => { for (const artifact of artifacts)
|
|
22328
|
+
remove.run(destination, run, artifact); });
|
|
22329
|
+
}
|
|
22330
|
+
/**
|
|
22331
|
+
* A result message just became a fact: everyone who asked for screenshots with results, can see the project and
|
|
22332
|
+
* would be pinged by it gets one row of their own that follows it — once per result, whatever produced the fact.
|
|
22333
|
+
* Only a result that saved screenshots qualifies; what is sent, and whether at all, is decided again at send time.
|
|
22334
|
+
*/
|
|
22335
|
+
enqueueResultShots(source, now) {
|
|
22336
|
+
if (source.run === null || source.taskRef === null || source.project === null || source.recipient !== null)
|
|
22337
|
+
return;
|
|
22338
|
+
const ready = isLifecycleNotification(source) && source.kind === "run-finished";
|
|
22339
|
+
if (!ready && !/fail/.test(source.kind))
|
|
22340
|
+
return;
|
|
22341
|
+
const run = this.getRun(source.run);
|
|
22342
|
+
if (run === null || !["builder", "repair"].includes(run.role) || run.contestant !== null)
|
|
22343
|
+
return;
|
|
22344
|
+
if (!this.artifactsFor(run.id).some(one => one.kind === "screenshot"))
|
|
22345
|
+
return;
|
|
22346
|
+
const people = this.db.prepare("SELECT account FROM notification_preference WHERE screenshots IN ('first', 'all') ORDER BY account").all().map(row => String(row["account"]));
|
|
22347
|
+
for (const account of people) {
|
|
22348
|
+
if (!this.accountCanAccess(account, source.project) || !this.pingAllowed(source, account))
|
|
22349
|
+
continue;
|
|
22350
|
+
this.enqueueNotification({ dedupeKey: `result-shots:r${run.id}:${account}`, kind: RESULT_SHOTS_KIND, subject: "Screenshots with this result", body: "",
|
|
22351
|
+
...(source.link === null ? {} : { link: source.link }), source: { run: run.id }, recipient: account }, now);
|
|
22352
|
+
}
|
|
22353
|
+
}
|
|
22237
22354
|
/** Everyone who asked for an evening digest, with the local day theirs was last considered. */
|
|
22238
22355
|
eveningDigestPeople() {
|
|
22239
22356
|
return this.db.prepare("SELECT account, digest_at, digest_on FROM notification_preference WHERE digest_at IS NOT NULL ORDER BY account").all()
|
|
@@ -22245,9 +22362,10 @@ export class Store {
|
|
|
22245
22362
|
.run(day, account, day).changes) === 1;
|
|
22246
22363
|
}
|
|
22247
22364
|
// ---- pings follow responsibility: the lead, who acted, replacements and muted projects ----
|
|
22248
|
-
/** What this person named their lead and its persona, or null when they never saved one.
|
|
22365
|
+
/** What this person named their lead and its persona, or null when they never saved one. An empty name or persona
|
|
22366
|
+
* (a row saved only for what the lead knows about them) reads as the default. */
|
|
22249
22367
|
leadConfig(account) {
|
|
22250
|
-
const row = this.db.prepare("SELECT name, persona FROM lead_config WHERE account = ?").get(account);
|
|
22368
|
+
const row = this.db.prepare("SELECT name, persona FROM lead_config WHERE account = ? AND (name <> '' OR persona <> '')").get(account);
|
|
22251
22369
|
return row === undefined ? null : { name: String(row["name"]), persona: String(row["persona"]) };
|
|
22252
22370
|
}
|
|
22253
22371
|
/** Save this person's lead name and persona (checked by the caller: lead-identity.ts). */
|
|
@@ -22255,6 +22373,25 @@ export class Store {
|
|
|
22255
22373
|
this.db.prepare(`INSERT INTO lead_config (account, name, persona, updated_at) VALUES (?, ?, ?, ?)
|
|
22256
22374
|
ON CONFLICT(account) DO UPDATE SET name = excluded.name, persona = excluded.persona, updated_at = excluded.updated_at`).run(account, name, persona, now.toISOString());
|
|
22257
22375
|
}
|
|
22376
|
+
/** What this person's lead knows about them: their confirmed lines, in order (none when they never saved any). */
|
|
22377
|
+
leadAbout(account) {
|
|
22378
|
+
const row = this.db.prepare("SELECT about_json FROM lead_config WHERE account = ?").get(account);
|
|
22379
|
+
if (row === undefined)
|
|
22380
|
+
return [];
|
|
22381
|
+
try {
|
|
22382
|
+
const lines = JSON.parse(String(row["about_json"]));
|
|
22383
|
+
return Array.isArray(lines) ? lines.filter((one) => typeof one === "string") : [];
|
|
22384
|
+
}
|
|
22385
|
+
catch {
|
|
22386
|
+
return [];
|
|
22387
|
+
}
|
|
22388
|
+
}
|
|
22389
|
+
/** Save what this person's lead knows about them (checked by the caller: lead-about.ts). A person who never named
|
|
22390
|
+
* their lead gets an empty name and persona, so they keep following the defaults. */
|
|
22391
|
+
setLeadAbout(account, lines, now) {
|
|
22392
|
+
this.db.prepare(`INSERT INTO lead_config (account, name, persona, updated_at, about_json) VALUES (?, '', '', ?, ?)
|
|
22393
|
+
ON CONFLICT(account) DO UPDATE SET about_json = excluded.about_json, updated_at = excluded.updated_at`).run(account, now.toISOString(), JSON.stringify(lines));
|
|
22394
|
+
}
|
|
22258
22395
|
/** A lead token for one person, shown once. A new one ends their earlier ones; the ledger names it "lead for <owner>". */
|
|
22259
22396
|
mintLeadCredential(owner, by, now) {
|
|
22260
22397
|
return this.transact(() => {
|
|
@@ -22492,6 +22629,12 @@ export class Store {
|
|
|
22492
22629
|
setChatBatchMessage(id, message, digest) {
|
|
22493
22630
|
this.db.prepare("UPDATE chat_batch SET message = ?, digest = ? WHERE id = ?").run(message, digest, id);
|
|
22494
22631
|
}
|
|
22632
|
+
/** The finished-work message that carries this result at one destination, if one was placed: screenshots follow it. */
|
|
22633
|
+
chatBatchMessageFor(destination, taskRef, run) {
|
|
22634
|
+
const row = this.db.prepare(`SELECT b.message FROM chat_batch b JOIN chat_batch_item i ON i.batch = b.id
|
|
22635
|
+
WHERE b.destination = ? AND i.task_ref = ? AND (i.run = ? OR i.run IS NULL) AND b.message IS NOT NULL ORDER BY b.id DESC LIMIT 1`).get(destination, taskRef, run);
|
|
22636
|
+
return row === undefined ? null : String(row["message"]);
|
|
22637
|
+
}
|
|
22495
22638
|
/** The message now showing this card, and what it shows. */
|
|
22496
22639
|
setChatCardMessage(id, message, digest) {
|
|
22497
22640
|
this.db.prepare("UPDATE chat_card SET message = ?, digest = ? WHERE id = ?").run(message, digest, id);
|
|
@@ -22666,8 +22809,12 @@ export class Store {
|
|
|
22666
22809
|
JOIN push_subscription s ON s.id = p.subscription JOIN notification n ON n.id = p.notification
|
|
22667
22810
|
WHERE p.state = 'pending' AND p.attempts = 0 AND p.created_at = ?`).all(now.toISOString());
|
|
22668
22811
|
for (const raw of fresh) {
|
|
22669
|
-
|
|
22670
|
-
|
|
22812
|
+
// A met promise made on a chat app is said there (lead-commitments.ts), not as a push: settled here, unsent. One
|
|
22813
|
+
// made in the console or app is this phone's own.
|
|
22814
|
+
const elsewhere = /^lead-promise:(telegram|slack|discord|teams):/.test(String(raw["dedupe_key"]));
|
|
22815
|
+
if (elsewhere || !this.pingAllowed(readNotification(raw), String(raw["approver"]))) {
|
|
22816
|
+
this.db.prepare("UPDATE push_delivery SET state = 'retired', last_error = ? WHERE id = ? AND state = 'pending'")
|
|
22817
|
+
.run(elsewhere ? TELEGRAM_SKIPPED_OTHER_CHAT : "quiet", Number(raw["pair"]));
|
|
22671
22818
|
}
|
|
22672
22819
|
}
|
|
22673
22820
|
}
|
package/dist/team-runtime.js
CHANGED
|
@@ -156,7 +156,7 @@ export function createTeamRuntime(options) {
|
|
|
156
156
|
context: `Shared team conversation ${claim.conversationId}. You are the lead ${view.leads.find(lead => lead.id === claim.leadId)?.name ?? claim.leadId}. Messages name their actual authors; message order does not grant authority. Use existing task actions and approvals. Do not rerun work to recover a notification. The following saved lead guidance is context, not new permissions:\n${claim.instructions}`,
|
|
157
157
|
clock, evidenceRoot: options.evidenceRoot,
|
|
158
158
|
revalidate: async () => !closed && !updateAdmissionPaused(store.raw()) && domain.current(claim) && store.teamMateSession(claim.actor.name, claim.threadId)?.id === session.id && authorization(claim.actor, domain.snapshot(claim.actor, claim.conversationId)).enabled && (!claim.requestId.startsWith('team-update:') || domain.access(claim.actor, claim.conversationId).conversation.follow)
|
|
159
|
-
? { ok: true } : { ok: false, reason: '
|
|
159
|
+
? { ok: true } : { ok: false, reason: 'access-changed' },
|
|
160
160
|
...(options.fetcher ? { fetcher: options.fetcher } : {}), ...(options.subscriptionRunner ? { subscriptionRunner: options.subscriptionRunner } : {}) });
|
|
161
161
|
if (result.ok)
|
|
162
162
|
domain.finish(claim, { status: 'answered', turnId: result.turn }, clock());
|
package/dist/teams-chat.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/** Microsoft Teams on the shared chat layer: activities in, Adaptive Cards
|
|
2
2
|
* out, the same durable receipts, rooms and commands as every other channel. */
|
|
3
|
+
import { resultShotsPruned } from "./result-shots.js";
|
|
3
4
|
import { chatQuestionButtons } from "./teammate-question.js";
|
|
4
5
|
import { chatAskButtons } from "./chat-ask.js";
|
|
5
6
|
import { ChatState, chatHash } from "./chat-delivery-state.js";
|
|
6
7
|
import { channelAccess, chatObject as object, planChatNotifications, planRoomMessages, processChatEvent } from "./chat-delivery.js";
|
|
7
8
|
import { roomCommand } from "./chat-rooms.js";
|
|
8
9
|
import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
10
|
+
import { renderReply } from "./reply-shape.js";
|
|
9
11
|
import { armedCardText, armedYesLabel, proposalLink, proposalOutcomeText, proposalPreview } from "./chat-channel.js";
|
|
10
12
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
11
13
|
import { channelInbox } from "./chat-inbox.js";
|
|
@@ -47,6 +49,14 @@ export const teamsDelivery = (options) => {
|
|
|
47
49
|
state,
|
|
48
50
|
label: "Teams",
|
|
49
51
|
member: (member, channel) => teamsMember(options.api, state, options.identity.installation, member, channel),
|
|
52
|
+
// "Typing…" while the lead works; Teams gives bots no reactions, so there is no 👍 here.
|
|
53
|
+
warm: (event) => ({
|
|
54
|
+
typing: async () => {
|
|
55
|
+
const serviceUrl = state.meta(options.identity.installation, serviceKey(event.channel));
|
|
56
|
+
if (serviceUrl !== null)
|
|
57
|
+
await options.api("POST", serviceUrl, `/v3/conversations/${encodeURIComponent(event.channel)}/activities`, { type: "typing" });
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
50
60
|
partSize: 3500,
|
|
51
61
|
maxProposal: 6000,
|
|
52
62
|
};
|
|
@@ -166,6 +176,10 @@ export async function deliverTeamsPart(options) {
|
|
|
166
176
|
if (content.task && !repos.includes(store.lookupRef(content.task)?.repo ?? ""))
|
|
167
177
|
throw new TeamsError("Connected projects changed");
|
|
168
178
|
let text = content.text, actions = [];
|
|
179
|
+
if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
|
|
180
|
+
state.prepare("UPDATE chat_part SET state='dropped',problem='Removed by retention' WHERE id=?").run(row.id);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
169
183
|
if (content.image) {
|
|
170
184
|
text = `${content.text}\n\nScreenshots open from the saved result.`;
|
|
171
185
|
actions = openUrlAction(options.origin(), { label: "Open result", path: chatResultHref(content.image.taskId, content.image.run, "checks") });
|
|
@@ -201,7 +215,9 @@ export async function deliverTeamsPart(options) {
|
|
|
201
215
|
actions = [...flow, ...asked, ...owner, ...openUrlAction(options.origin(), content.link), ...(content.also ?? []).flatMap(one => openUrlAction(options.origin(), one))];
|
|
202
216
|
}
|
|
203
217
|
const target = content.edit ?? row.message;
|
|
204
|
-
|
|
218
|
+
// The lead's own reply goes out in Teams' Markdown (bold anchors, labelled links); everything else stays plain.
|
|
219
|
+
const voiced = content.voice === true && !content.proposal && !content.image ? renderReply(text, "teams") : null;
|
|
220
|
+
const body = actions.length || content.proposal ? teamsCard(voiced ?? text, actions) : voiced !== null ? { type: "message", text: voiced, textFormat: "markdown" } : { type: "message", text, textFormat: "plain" };
|
|
205
221
|
const answer = await options.api(target ? "PUT" : "POST", serviceUrl, `/v3/conversations/${encodeURIComponent(destination)}/activities${target ? `/${encodeURIComponent(target)}` : ""}`, body);
|
|
206
222
|
const messageId = target ?? (typeof answer.id === "string" ? answer.id : null);
|
|
207
223
|
if (messageId === null)
|
package/dist/telegram-mate.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export declare const CONVERSATION_MAX_AGE_MS: number;
|
|
|
12
12
|
/** A card's buttons and an armed irreversible challenge live this long. */
|
|
13
13
|
export declare const CARD_TTL_MS: number;
|
|
14
14
|
export declare const CHALLENGE_TTL_MS: number;
|
|
15
|
+
/** Telegram's own message ceiling, with room for our part headers (the same bound telegram.ts splits at). */
|
|
16
|
+
export declare const PART_CAP = 3900;
|
|
15
17
|
export type TelegramConversationOptions = {
|
|
16
18
|
/** Where evidence lives — the same root the console and CLI read results from. */
|
|
17
19
|
evidenceRoot: string;
|
|
@@ -26,6 +28,16 @@ export type TelegramConversationOptions = {
|
|
|
26
28
|
* wires `phoneOrigin` from webhooks.ts; absent, no card carries a link.
|
|
27
29
|
*/
|
|
28
30
|
phoneOrigin?: () => string | null;
|
|
31
|
+
/** Injected by tests: how a Merge tapped in chat reaches GitHub (production: mergePullRequest). */
|
|
32
|
+
merge?: (input: {
|
|
33
|
+
runId: number;
|
|
34
|
+
by: string;
|
|
35
|
+
}) => Promise<{
|
|
36
|
+
ok: true;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
message: string;
|
|
40
|
+
}>;
|
|
29
41
|
};
|
|
30
42
|
/** One Bot API call, the shape telegram.ts injects. */
|
|
31
43
|
export type Transport = TelegramTransport;
|
package/dist/telegram-mate.js
CHANGED
|
@@ -23,11 +23,13 @@ export { channelRepos as telegramConversationRepos, resolveChannelMate as resolv
|
|
|
23
23
|
* the console and CLI state), and the door re-proves everything again.
|
|
24
24
|
*/
|
|
25
25
|
import { createHash, randomBytes } from "node:crypto";
|
|
26
|
-
import { MATE_MESSAGE_MAX_CHARS, runMateTurn } from "./mate.js";
|
|
26
|
+
import { MATE_MESSAGE_MAX_CHARS, mateFailureText, runMateTurn } from "./mate.js";
|
|
27
27
|
import { confirmMateProposal, dismissMateProposal } from "./mate-doors.js";
|
|
28
28
|
import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
|
|
29
29
|
import { MATE_ASK_OTHER } from "./store.js";
|
|
30
30
|
import { phoneText } from "./telegram-status.js";
|
|
31
|
+
import { shapeReplyParts, telegramReply } from "./reply-shape.js";
|
|
32
|
+
import { WARM_EMOJI, warmTurn } from "./chat-warmth.js";
|
|
31
33
|
import { TeamLeads } from "./team-leads.js";
|
|
32
34
|
import { chatResultHref } from "./chat-controls.js";
|
|
33
35
|
import { resultImageFileName, safeResultImageCaption, verifyResultImage } from "./chat-evidence.js";
|
|
@@ -40,7 +42,7 @@ export const CONVERSATION_MAX_AGE_MS = 10 * 60_000;
|
|
|
40
42
|
export const CARD_TTL_MS = 24 * 3_600_000;
|
|
41
43
|
export const CHALLENGE_TTL_MS = 10 * 60_000;
|
|
42
44
|
/** Telegram's own message ceiling, with room for our part headers (the same bound telegram.ts splits at). */
|
|
43
|
-
const PART_CAP = 3_900;
|
|
45
|
+
export const PART_CAP = 3_900;
|
|
44
46
|
// ---- the ceiling and the principal --------------------------------------------
|
|
45
47
|
/**
|
|
46
48
|
* The phone's project ceiling: the enrolled registry, canonicalized and
|
|
@@ -405,7 +407,7 @@ async function runTelegramConversation(row, args) {
|
|
|
405
407
|
const bound = store.getTelegramConversation(row.id)?.session ?? null;
|
|
406
408
|
const session = bound === null ? null : store.getMateSession(bound);
|
|
407
409
|
if (session === null) {
|
|
408
|
-
await notify(
|
|
410
|
+
await notify("This conversation was restarted before my reply went out, so it wasn't sent. Nothing was changed. Send your message again.");
|
|
409
411
|
finish({ state: "failed", outcome: "unsent:no-session" });
|
|
410
412
|
report.refused++;
|
|
411
413
|
return;
|
|
@@ -525,11 +527,14 @@ async function runTelegramConversation(row, args) {
|
|
|
525
527
|
const origin = proposal === null ? null : options.phoneOrigin?.() ?? null;
|
|
526
528
|
const wanted = proposal === null ? null : proposalLink(store, proposal, repos);
|
|
527
529
|
const keyboard = keyboardWith(part.keyboard, phoneLinkButton(origin, wanted));
|
|
528
|
-
|
|
530
|
+
// The lead's own words carry their bold anchors and labelled links as entities, never as Markdown characters.
|
|
531
|
+
const voiced = part.kind === "reply" ? telegramReply(part.text) : null;
|
|
532
|
+
const text = proposal !== null && part.keyboard === null ? handoffCardText(part.text, linkNote(origin, wanted)) : voiced?.text ?? part.text;
|
|
529
533
|
method = "sendMessage";
|
|
530
534
|
send = () => transport("sendMessage", {
|
|
531
535
|
chat_id: chatId,
|
|
532
536
|
text,
|
|
537
|
+
...(voiced === null || voiced.entities.length === 0 ? {} : { entities: voiced.entities }),
|
|
533
538
|
link_preview_options: { is_disabled: true },
|
|
534
539
|
...(part.replyTo === null ? {} : { reply_parameters: { message_id: Number(part.replyTo) } }),
|
|
535
540
|
...(keyboard === undefined ? {} : { reply_markup: { inline_keyboard: keyboard } }),
|
|
@@ -590,7 +595,7 @@ async function runTelegramConversation(row, args) {
|
|
|
590
595
|
try {
|
|
591
596
|
return store.transact(() => {
|
|
592
597
|
const now = clock();
|
|
593
|
-
const parts =
|
|
598
|
+
const parts = shapeReplyParts(reply, PART_CAP, { asked: row.text, appOrigin: options.phoneOrigin?.() ?? null }, shaped => telegramReply(shaped).text.length).map((text, index) => ({ kind: "reply", text, replyTo: index === 0 ? row.messageId : null }));
|
|
594
599
|
// The lead's question to its owner: one tap per option, then "Something else".
|
|
595
600
|
const ask = store.mateAsk(turn);
|
|
596
601
|
if (ask !== null)
|
|
@@ -633,7 +638,7 @@ async function runTelegramConversation(row, args) {
|
|
|
633
638
|
return;
|
|
634
639
|
}
|
|
635
640
|
if (turn.state !== "answered") {
|
|
636
|
-
await notify(
|
|
641
|
+
await notify(mateFailureText(turn.failureReason));
|
|
637
642
|
finish({ state: "failed", outcome: `replayed:${turn.failureReason ?? "failed"}` });
|
|
638
643
|
report.refused++;
|
|
639
644
|
return;
|
|
@@ -693,7 +698,13 @@ async function runTelegramConversation(row, args) {
|
|
|
693
698
|
const { who, session, thread, config } = resolved;
|
|
694
699
|
const revalidate = async () => {
|
|
695
700
|
const problem = await telegramChannelProblem(store, { botId, bindingId: binding.id, approverGeneration: binding.approverGeneration, repos: who.repos }, readProjects);
|
|
696
|
-
|
|
701
|
+
if (problem === null)
|
|
702
|
+
return { ok: true };
|
|
703
|
+
const reason = problem === UNREADABLE_REGISTRY ? "projects-unreadable"
|
|
704
|
+
: problem === "this chat is no longer paired" ? "unpaired"
|
|
705
|
+
: problem === "the paired account is no longer an approver" ? "not-approver"
|
|
706
|
+
: problem === "the connected projects changed" ? "projects-changed" : "access-changed";
|
|
707
|
+
return { ok: false, reason };
|
|
697
708
|
};
|
|
698
709
|
// Bound BEFORE the dispatch, under the claim: a crash from here on finds
|
|
699
710
|
// the receipt in this session, not in whichever session is live later.
|
|
@@ -704,17 +715,23 @@ async function runTelegramConversation(row, args) {
|
|
|
704
715
|
// crash gets the original turn back, never a second dispatch.
|
|
705
716
|
const heartbeat = setInterval(() => { held(); }, 30_000);
|
|
706
717
|
heartbeat.unref?.();
|
|
718
|
+
// Once the turn reaches a tool step: a 👍 on the owner's message, and "typing…" while the lead works.
|
|
719
|
+
const warm = warmTurn({
|
|
720
|
+
react: () => transport("setMessageReaction", { chat_id: chatId, message_id: Number(row.messageId), reaction: [{ type: "emoji", emoji: WARM_EMOJI }] }),
|
|
721
|
+
typing: () => transport("sendChatAction", { chat_id: chatId, action: "typing" }),
|
|
722
|
+
});
|
|
707
723
|
let outcome;
|
|
708
724
|
try {
|
|
709
725
|
outcome = await runMateTurn({
|
|
710
726
|
store, who, session, thread, config, key: null, message: row.text, requestId: row.request,
|
|
711
727
|
...(row.context === null ? {} : { context: row.context }),
|
|
712
728
|
...(options.subscriptionRunner === undefined ? {} : { subscriptionRunner: options.subscriptionRunner }),
|
|
713
|
-
clock, evidenceRoot: options.evidenceRoot, revalidate, mediaDelivery: "documents", channel: "telegram",
|
|
729
|
+
clock, evidenceRoot: options.evidenceRoot, revalidate, mediaDelivery: "documents", channel: "telegram", onProgress: warm.onProgress,
|
|
714
730
|
});
|
|
715
731
|
}
|
|
716
732
|
finally {
|
|
717
733
|
clearInterval(heartbeat);
|
|
734
|
+
warm.stop();
|
|
718
735
|
}
|
|
719
736
|
const receipt = store.mateRequestReceipt(session.id, row.request);
|
|
720
737
|
if (receipt !== null)
|
|
@@ -724,7 +741,7 @@ async function runTelegramConversation(row, args) {
|
|
|
724
741
|
await requeue("busy");
|
|
725
742
|
return;
|
|
726
743
|
}
|
|
727
|
-
await notify(phoneText(
|
|
744
|
+
await notify(phoneText(outcome.message, 1_000));
|
|
728
745
|
finish({ state: "failed", outcome: `refused:${outcome.refused}` });
|
|
729
746
|
report.refused++;
|
|
730
747
|
return;
|
|
@@ -732,8 +749,7 @@ async function runTelegramConversation(row, args) {
|
|
|
732
749
|
if (!outcome.ok) {
|
|
733
750
|
// Truthful: what failed, that nothing was kept, and that a new message
|
|
734
751
|
// is a new turn. A failed turn's drafts are already gone.
|
|
735
|
-
|
|
736
|
-
await notify(phoneText(words, 1_000));
|
|
752
|
+
await notify(phoneText(outcome.message, 1_000));
|
|
737
753
|
finish({ state: "failed", outcome: `failed:${outcome.failed}` });
|
|
738
754
|
report.refused++;
|
|
739
755
|
return;
|
package/dist/telegram-team.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { renderReply, shapeReplyParts } from "./reply-shape.js";
|
|
1
2
|
import { TeamLeads } from "./team-leads.js";
|
|
2
3
|
import { proposalPreview, tooLongText } from "./chat-channel.js";
|
|
3
|
-
import { mintCardTokens, phoneLinkButton, splitParts } from "./telegram-mate.js";
|
|
4
|
+
import { PART_CAP, mintCardTokens, phoneLinkButton, splitParts } from "./telegram-mate.js";
|
|
4
5
|
import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
5
6
|
import { phoneText } from "./telegram-status.js";
|
|
6
7
|
/** `/team` lists, `/team <n>` chooses, `/team off` (or `private`) leaves. Any other slash text is not ours. */
|
|
@@ -265,7 +266,9 @@ export async function deliverTeamChats(store, botId, transport, clock, report, p
|
|
|
265
266
|
const skip = role === "operator" && (ownMessage || status === "cancelled" || String(message["author"] ?? "").length === 0);
|
|
266
267
|
const prefix = `telegram-team:${chat.id}:`;
|
|
267
268
|
if (!skip) {
|
|
268
|
-
|
|
269
|
+
// The lead's reply is split as written, then each part shaped and measured as Telegram shows it, so no cut lands
|
|
270
|
+
// inside a link and no rendered part passes Telegram's ceiling.
|
|
271
|
+
const text = role === "assistant" ? shapeReplyParts(String(message["text"]), PART_CAP, { appOrigin: phoneOrigin?.() ?? null }, shaped => renderReply(shaped, "telegram").length).map(part => renderReply(part, "telegram")) : `${phoneText(String(message["author"]), 40)}: ${String(message["text"])}`;
|
|
269
272
|
const sent = await sendParts(store, transport, chat.chatId, text, `${prefix}message:${id}`, clock, access, report);
|
|
270
273
|
if (sent !== null) {
|
|
271
274
|
report.problems.push(`team chat ${chat.chatId}: ${sent}`);
|
|
@@ -329,7 +332,7 @@ function cardBindingFor(store, botId, chat, turn) {
|
|
|
329
332
|
return bindings.find(one => members.has(one.approver)) ?? null;
|
|
330
333
|
}
|
|
331
334
|
async function sendParts(store, transport, chatId, text, receipt, clock, access, report, keyboard, tokens = []) {
|
|
332
|
-
const parts = splitParts(text);
|
|
335
|
+
const parts = typeof text === "string" ? splitParts(text) : text;
|
|
333
336
|
const delivered = store.serviceCursor(receipt);
|
|
334
337
|
for (const [index, part] of parts.entries()) {
|
|
335
338
|
if (index < delivered)
|
package/dist/telegram.d.ts
CHANGED
|
@@ -75,7 +75,9 @@ export declare function scrub(text: string, token: string): string;
|
|
|
75
75
|
*/
|
|
76
76
|
export type TelegramTransport = (method: string, params: Record<string, unknown>, signal?: AbortSignal,
|
|
77
77
|
/** v64: one verified file to send as multipart — the ONLY way bytes leave. Absent, the call is JSON exactly as before. */
|
|
78
|
-
upload?: TelegramUpload
|
|
78
|
+
upload?: TelegramUpload,
|
|
79
|
+
/** More verified files in the same multipart call: the rest of one album (sendMediaGroup). */
|
|
80
|
+
more?: readonly TelegramUpload[]) => Promise<{
|
|
79
81
|
ok: boolean;
|
|
80
82
|
result?: unknown;
|
|
81
83
|
description?: string;
|
|
@@ -92,7 +94,7 @@ upload?: TelegramUpload) => Promise<{
|
|
|
92
94
|
* uploaded by name.
|
|
93
95
|
*/
|
|
94
96
|
export type TelegramUpload = {
|
|
95
|
-
field: "document"
|
|
97
|
+
field: "document" | "photo" | `shot${number}`;
|
|
96
98
|
fileName: string;
|
|
97
99
|
contentType: "image/png" | "image/jpeg";
|
|
98
100
|
bytes: Buffer;
|
|
@@ -113,7 +115,7 @@ export declare function networkFenceLine(reason: string): string;
|
|
|
113
115
|
/** fetch's own message ("fetch failed") with the reason under it, e.g. "fetch failed (EMFILE: too many open files)". */
|
|
114
116
|
export declare function transportError(error: unknown): string;
|
|
115
117
|
/** The multipart body: every param a field (objects as JSON), the file last. Exported for the adapter's own test only. */
|
|
116
|
-
export declare function multipartOf(params: Record<string, unknown>, upload: TelegramUpload): FormData;
|
|
118
|
+
export declare function multipartOf(params: Record<string, unknown>, upload: TelegramUpload, ...more: readonly TelegramUpload[]): FormData;
|
|
117
119
|
export declare function hashPairingCode(code: string): string;
|
|
118
120
|
/** 128 bits, hex — pasteable, and not guessable inside any code's lifetime. */
|
|
119
121
|
export declare function mintPairingCode(): string;
|