toolroll 0.9.96 → 0.9.97
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 +8 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/chat-delivery.d.ts +5 -1
- package/dist/chat-delivery.js +74 -25
- package/dist/chat-follow.d.ts +6 -3
- package/dist/chat-follow.js +23 -7
- package/dist/chat-live-schema.d.ts +17 -0
- package/dist/chat-live-schema.js +32 -0
- package/dist/chat-live.d.ts +161 -0
- package/dist/chat-live.js +491 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-api.d.ts +3 -1
- package/dist/discord-api.js +9 -3
- package/dist/discord-chat.d.ts +13 -3
- package/dist/discord-chat.js +77 -21
- package/dist/discord.js +8 -1
- package/dist/lead-answer-eval.d.ts +86 -0
- package/dist/lead-answer-eval.js +255 -0
- package/dist/lead-cli-live.d.ts +56 -0
- package/dist/lead-cli-live.js +200 -0
- package/dist/lead-cli.d.ts +3 -0
- package/dist/lead-cli.js +34 -4
- package/dist/lead-latency-schema.d.ts +24 -0
- package/dist/lead-latency-schema.js +36 -0
- package/dist/lead-progress.d.ts +10 -1
- package/dist/lead-progress.js +1 -1
- package/dist/lead-queue-runtime.d.ts +1 -1
- package/dist/lead-queue-runtime.js +1 -1
- package/dist/lead-route.d.ts +56 -0
- package/dist/lead-route.js +104 -0
- package/dist/lead.d.ts +4 -2
- package/dist/lead.js +79 -26
- package/dist/live-reply.d.ts +83 -0
- package/dist/live-reply.js +302 -0
- package/dist/operate.js +39 -1
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/reply-shape.d.ts +9 -0
- package/dist/reply-shape.js +13 -0
- package/dist/serve.js +11 -4
- package/dist/server/chat.d.ts +2 -1
- package/dist/server/chat.js +18 -6
- package/dist/server/live.js +18 -21
- package/dist/server/render-chat.d.ts +5 -0
- package/dist/server/render-chat.js +4 -2
- package/dist/server/render-settings.d.ts +8 -0
- package/dist/server/render-settings.js +13 -2
- package/dist/server/route-table.js +2 -0
- package/dist/server/settings.js +14 -1
- package/dist/slack-api.js +11 -0
- package/dist/slack-chat.d.ts +11 -1
- package/dist/slack-chat.js +50 -5
- package/dist/slack-settings.js +8 -1
- package/dist/slack.js +10 -3
- package/dist/store.d.ts +31 -2
- package/dist/store.js +59 -6
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +3 -1
- package/dist/teams-chat.d.ts +9 -2
- package/dist/teams-chat.js +50 -2
- package/dist/teams.js +5 -2
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +63 -9
- package/dist/telegram-live.d.ts +116 -0
- package/dist/telegram-live.js +224 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +50 -2
- package/dist/toolroll-update.js +4 -1
- package/package.json +1 -1
package/dist/slack-chat.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { ChatState } from "./chat-delivery-state.js";
|
|
1
|
+
import { ChatState, type ChatEvent } from "./chat-delivery-state.js";
|
|
2
2
|
import { uploadSlackBytes, type SlackApi, type SlackIdentity } from "./slack-api.js";
|
|
3
3
|
import { type Store } from "./store.js";
|
|
4
|
+
import { type ChatAckHooks, type ChatLiveTransport, type LiveReplies } from "./chat-live.js";
|
|
4
5
|
import type { SubscriptionLeadRunner } from "./subscription-chat.js";
|
|
5
6
|
export type SlackChatOptions = {
|
|
6
7
|
store: Store;
|
|
@@ -15,6 +16,8 @@ export type SlackChatOptions = {
|
|
|
15
16
|
canNotify?: () => boolean;
|
|
16
17
|
clock?: () => Date;
|
|
17
18
|
upload?: typeof uploadSlackBytes;
|
|
19
|
+
/** The live "Working on it…" message (chat-live.ts): on in the worker, off unless asked for. */
|
|
20
|
+
liveReplies?: LiveReplies;
|
|
18
21
|
};
|
|
19
22
|
/** Synchronous receipt before ACK. Never stores response_url, raw tokens, or a pairing code. */
|
|
20
23
|
export declare function receiveSlack(state: ChatState, identity: SlackIdentity, type: string, raw: unknown, now: Date): boolean;
|
|
@@ -25,3 +28,10 @@ export declare function slackBlocks(text: string, buttons?: Record<string, unkno
|
|
|
25
28
|
/** The lead's reply as mrkdwn sections: no header block, so its first line stays an ordinary line. */
|
|
26
29
|
export declare function slackVoiceBlocks(mrkdwn: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
|
|
27
30
|
export declare function deliverSlackPart(options: SlackChatOptions): Promise<boolean>;
|
|
31
|
+
/** A live message's exact payload (§3.2): the rendered mrkdwn as the top-level text and as verbatim sections, every
|
|
32
|
+
* field sent on every edit, nothing parsed, linked or unfurled, no buttons. */
|
|
33
|
+
export declare function slackLivePayload(channel: string, mrkdwn: string): Record<string, unknown>;
|
|
34
|
+
/** The live message for one owner message, in its thread (where the reply goes). */
|
|
35
|
+
export declare function slackLive(options: Pick<SlackChatOptions, "api">, event: ChatEvent, spacingMs?: number): ChatLiveTransport;
|
|
36
|
+
/** The 👀 on the owner's message (reactions:write). An install without that scope is asked at most once a minute, quietly. */
|
|
37
|
+
export declare function slackAckHooks(options: Pick<SlackChatOptions, "api">): ChatAckHooks;
|
package/dist/slack-chat.js
CHANGED
|
@@ -17,7 +17,9 @@ import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./con
|
|
|
17
17
|
import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
|
|
18
18
|
import { resultImageFileName, safeResultImageCaption, verifyResultImage, } from "./chat-evidence.js";
|
|
19
19
|
import {} from "./store.js";
|
|
20
|
+
import { isLiveMessage, liveSpacingOf } from "./chat-live.js";
|
|
20
21
|
const SLACK_REFUSED_BLOCKS = new Set(["invalid_blocks", "invalid_blocks_format", "invalid_arguments", "msg_too_long", "msg_blocks_too_long"]);
|
|
22
|
+
const SLACK_UNEDITABLE = new Set(["message_not_found", "cant_update_message", "edit_window_closed"]);
|
|
21
23
|
const nowOf = (options) => options.clock?.() ?? new Date();
|
|
22
24
|
const split = (text, size = 2800) => {
|
|
23
25
|
const parts = [];
|
|
@@ -125,10 +127,8 @@ const delivery = (options) => ({
|
|
|
125
127
|
state: new ChatState(options.store, "slack"),
|
|
126
128
|
label: "Slack",
|
|
127
129
|
member: (member, channel) => slackMember(options.api, options.identity, member, channel),
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
react: () => options.api("reactions.add", { channel: event.channel, timestamp: event.ts, name: "thumbsup" }),
|
|
131
|
-
}),
|
|
130
|
+
// Slack shows bots no typing indicator; the 👀 goes at arrival (slackAckHooks) and the live message shows the rest.
|
|
131
|
+
...(options.liveReplies ? { live: (event) => slackLive(options, event, liveSpacingOf(options.liveReplies)) } : {}),
|
|
132
132
|
});
|
|
133
133
|
export const processSlackEvent = (options) => processChatEvent(delivery(options));
|
|
134
134
|
export const planSlackNotifications = (options) => planChatNotifications(delivery(options));
|
|
@@ -157,7 +157,8 @@ export function slackBlocks(text, buttons = []) {
|
|
|
157
157
|
export function slackVoiceBlocks(mrkdwn, buttons = []) {
|
|
158
158
|
const blocks = split(mrkdwn).map((part) => ({
|
|
159
159
|
type: "section",
|
|
160
|
-
|
|
160
|
+
// verbatim: Slack never turns words of the reply into a mention, a channel or a link (renderReply escapes the rest).
|
|
161
|
+
text: { type: "mrkdwn", text: part, verbatim: true },
|
|
161
162
|
}));
|
|
162
163
|
if (buttons.length)
|
|
163
164
|
blocks.push({ type: "actions", elements: buttons });
|
|
@@ -396,6 +397,7 @@ export async function deliverSlackPart(options) {
|
|
|
396
397
|
blocks: content.rich ? slackCardBlocks(content.rich, buttons) : voiced === null ? slackBlocks(text, buttons) : slackVoiceBlocks(voiced, buttons),
|
|
397
398
|
mrkdwn: voiced !== null,
|
|
398
399
|
parse: "none",
|
|
400
|
+
link_names: false,
|
|
399
401
|
unfurl_links: false,
|
|
400
402
|
unfurl_media: false,
|
|
401
403
|
};
|
|
@@ -423,6 +425,12 @@ export async function deliverSlackPart(options) {
|
|
|
423
425
|
}
|
|
424
426
|
// No permission to upload files here: one plain line instead of the result's screenshots.
|
|
425
427
|
const saved = readChatPart(row.payload), content = saved.ok ? saved.value : null;
|
|
428
|
+
// The live message the reply was to replace is gone (deleted, or too old to edit): the reply goes out as a new one.
|
|
429
|
+
if (content?.edit !== undefined && error instanceof SlackError && SLACK_UNEDITABLE.has(error.code) && isLiveMessage(state, row.event, content.edit)) {
|
|
430
|
+
const { edit: _edit, ...asNew } = content;
|
|
431
|
+
state.repaint(row.id, asNew);
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
426
434
|
// Slack refused the card's blocks: the part's own plain text goes instead, so the message is never lost.
|
|
427
435
|
if (content?.rich && error instanceof SlackError && SLACK_REFUSED_BLOCKS.has(error.code)) {
|
|
428
436
|
state.repaint(row.id, withoutCard(content));
|
|
@@ -444,3 +452,40 @@ export async function deliverSlackPart(options) {
|
|
|
444
452
|
return true;
|
|
445
453
|
}
|
|
446
454
|
}
|
|
455
|
+
/** A live message's exact payload (§3.2): the rendered mrkdwn as the top-level text and as verbatim sections, every
|
|
456
|
+
* field sent on every edit, nothing parsed, linked or unfurled, no buttons. */
|
|
457
|
+
export function slackLivePayload(channel, mrkdwn) {
|
|
458
|
+
return { channel, text: mrkdwn, blocks: slackVoiceBlocks(mrkdwn), mrkdwn: true, parse: "none", link_names: false, unfurl_links: false, unfurl_media: false };
|
|
459
|
+
}
|
|
460
|
+
/** The live message for one owner message, in its thread (where the reply goes). */
|
|
461
|
+
export function slackLive(options, event, spacingMs) {
|
|
462
|
+
return {
|
|
463
|
+
platform: "slack",
|
|
464
|
+
...(spacingMs === undefined ? {} : { spacingMs }),
|
|
465
|
+
limit: 2_800,
|
|
466
|
+
create: async (text) => {
|
|
467
|
+
const answer = await options.api("chat.postMessage", { ...slackLivePayload(event.channel, text), thread_ts: event.thread });
|
|
468
|
+
if (!slackTs(answer.ts))
|
|
469
|
+
throw new SlackError("Slack did not confirm the message identity", 15_000, true);
|
|
470
|
+
return answer.ts;
|
|
471
|
+
},
|
|
472
|
+
edit: async (message, text) => {
|
|
473
|
+
const answer = await options.api("chat.update", { ...slackLivePayload(event.channel, text), ts: message });
|
|
474
|
+
if (answer.ts !== message)
|
|
475
|
+
throw new SlackError("Slack did not confirm the message identity", 15_000, true);
|
|
476
|
+
},
|
|
477
|
+
rateLimited: error => error instanceof SlackError && error.code === "ratelimited" ? error.retryMs : null,
|
|
478
|
+
uncertain: error => !(error instanceof SlackError) || error.uncertain,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/** The 👀 on the owner's message (reactions:write). An install without that scope is asked at most once a minute, quietly. */
|
|
482
|
+
export function slackAckHooks(options) {
|
|
483
|
+
const code = (error) => error instanceof SlackError ? error.code : "";
|
|
484
|
+
return {
|
|
485
|
+
react: (event) => options.api("reactions.add", { channel: event.channel, timestamp: event.ts, name: "eyes" }),
|
|
486
|
+
unreact: (event) => options.api("reactions.remove", { channel: event.channel, timestamp: event.ts, name: "eyes" }),
|
|
487
|
+
refused: error => ["missing_scope", "no_permission", "not_in_channel", "channel_not_found"].includes(code(error)),
|
|
488
|
+
settled: error => ["already_reacted", "no_reaction", "message_not_found"].includes(code(error)),
|
|
489
|
+
rateLimited: error => code(error) === "ratelimited" ? error.retryMs : null,
|
|
490
|
+
};
|
|
491
|
+
}
|
package/dist/slack-settings.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { html, joinHtml } from "./html.js";
|
|
2
2
|
import { loadSlackCredentials } from "./slack-api.js";
|
|
3
3
|
import { chatSecretRow, chatSettingsHtml } from "./chat-settings-page.js";
|
|
4
|
+
import { ChatState } from "./chat-delivery-state.js";
|
|
5
|
+
import { ACK_REFUSED_META } from "./chat-live.js";
|
|
6
|
+
import { notice } from "./server/kit.js";
|
|
4
7
|
export function slackSettingsHtml(store, dir, options = {}) {
|
|
8
|
+
const credentials = loadSlackCredentials(dir);
|
|
9
|
+
// Slack answered "missing scope" to the 👀: an app installed before reactions:write needs one reinstall.
|
|
10
|
+
const reinstall = credentials !== null && new ChatState(store, "slack").meta(credentials.installation, ACK_REFUSED_META) !== null;
|
|
5
11
|
return chatSettingsHtml(store, dir, options, {
|
|
6
12
|
provider: "slack",
|
|
7
13
|
label: "Slack",
|
|
8
|
-
credentials
|
|
14
|
+
credentials,
|
|
15
|
+
...(reinstall ? { connection: notice(html `Reinstall needed for reactions. Update the app with the new <a class="k-link" href="/settings/slack/manifest">app manifest</a>, then reinstall it to your workspace.`) } : {}),
|
|
9
16
|
intro: "Manage projects and review results in a private Slack conversation.",
|
|
10
17
|
steps: html `<ol class="k-note"><li>Download the <a class="k-link" href="/settings/slack/manifest">app manifest</a>, then <a class="k-link" href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer">create an app</a> in your workspace using that manifest.</li><li>Under Basic Information, create an app-level token with <code class="k-mono">connections:write</code>.</li><li>Under OAuth & Permissions, install the app and copy its bot token.</li></ol>`,
|
|
11
18
|
fields: joinHtml([
|
package/dist/slack.js
CHANGED
|
@@ -3,7 +3,8 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { abortablePause, workChatQueue } from "./chat-follow.js";
|
|
4
4
|
import { loadSlackCredentials, slackApi } from "./slack-api.js";
|
|
5
5
|
import { ChatState } from "./chat-delivery-state.js";
|
|
6
|
-
import {
|
|
6
|
+
import { runChatAcks } from "./chat-live.js";
|
|
7
|
+
import { receiveSlack, processSlackEvent, deliverSlackPart, planSlackNotifications, planSlackRooms, slackAckHooks, } from "./slack-chat.js";
|
|
7
8
|
export async function followSlack(options) {
|
|
8
9
|
const owner = randomBytes(16).toString("hex"), state = new ChatState(options.store, "slack");
|
|
9
10
|
const pause = abortablePause(options.signal);
|
|
@@ -74,11 +75,16 @@ export async function followSlack(options) {
|
|
|
74
75
|
problem("Reconnecting to Slack…");
|
|
75
76
|
});
|
|
76
77
|
client.on("error", () => problem("Slack connection failed. Check the app tokens and reconnect."));
|
|
78
|
+
// The 👀 on each owner message as it arrives, beside the queue (a turn there can take a minute).
|
|
79
|
+
const api = slackApi(credentials.botToken);
|
|
80
|
+
const acks = runChatAcks({ state, installation: credentials.installation, owner, current: () => same() && connected,
|
|
81
|
+
hooks: slackAckHooks({ api }), same, signal: options.signal });
|
|
77
82
|
client.on("slack_event", (envelope) => {
|
|
78
83
|
if (!same())
|
|
79
84
|
return;
|
|
80
85
|
try {
|
|
81
|
-
receiveSlack(state, credentials, envelope.type, envelope.body, new Date())
|
|
86
|
+
if (receiveSlack(state, credentials, envelope.type, envelope.body, new Date()))
|
|
87
|
+
acks.wake();
|
|
82
88
|
void envelope
|
|
83
89
|
.ack()
|
|
84
90
|
.catch(() => problem("Slack acknowledgement was lost; a repeated event will be recognized."));
|
|
@@ -91,9 +97,10 @@ export async function followSlack(options) {
|
|
|
91
97
|
...options,
|
|
92
98
|
identity: credentials,
|
|
93
99
|
owner,
|
|
94
|
-
api
|
|
100
|
+
api,
|
|
95
101
|
canNotify: options.notifications,
|
|
96
102
|
current: same,
|
|
103
|
+
liveReplies: true,
|
|
97
104
|
};
|
|
98
105
|
try {
|
|
99
106
|
await client.start();
|
package/dist/store.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { type AcpLaunch } from "./acp-attest.js";
|
|
|
8
8
|
import { type CheckStamp, type StampReading } from "./check-stamp.js";
|
|
9
9
|
import { V155_STEPS } from "./fleet-schema.js";
|
|
10
10
|
import { V156_STEPS } from "./fallback-schema.js";
|
|
11
|
+
import { V158_STEPS, type LeadFailureDetail, type LeadRoute } from "./lead-latency-schema.js";
|
|
12
|
+
import { V157_STEPS } from "./chat-live-schema.js";
|
|
11
13
|
export { refArchived, taskArchived } from "./task-archive.js";
|
|
12
14
|
import { type ChatBinding } from "./chat-core.js";
|
|
13
15
|
import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET } from "./chat-messages.js";
|
|
@@ -248,7 +250,7 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
248
250
|
hash: string;
|
|
249
251
|
} | null;
|
|
250
252
|
};
|
|
251
|
-
export declare const SCHEMA_VERSION =
|
|
253
|
+
export declare const SCHEMA_VERSION = 158;
|
|
252
254
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
253
255
|
export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
|
|
254
256
|
/**
|
|
@@ -440,7 +442,7 @@ export declare const V147_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
|
|
|
440
442
|
}>>>>;
|
|
441
443
|
/** The v147 migration's steps after the epoch, in order: a test seam (OpenOptions.migrationStep) may stop after each. */
|
|
442
444
|
export declare const V147_STEPS: readonly ["epoch", "local-mode", "password-set", "web-session", "console-instance"];
|
|
443
|
-
export type MigrationStep = (typeof V147_STEPS)[number] | (typeof V148_STEPS)[number] | (typeof V149_STEPS)[number] | (typeof V150_STEPS)[number] | (typeof V151_STEPS)[number] | (typeof V152_STEPS)[number] | (typeof V153_STEPS)[number] | (typeof V154_STEPS)[number] | (typeof V156_STEPS)[number] | (typeof V155_STEPS)[number];
|
|
445
|
+
export type MigrationStep = (typeof V147_STEPS)[number] | (typeof V148_STEPS)[number] | (typeof V149_STEPS)[number] | (typeof V150_STEPS)[number] | (typeof V151_STEPS)[number] | (typeof V152_STEPS)[number] | (typeof V153_STEPS)[number] | (typeof V154_STEPS)[number] | (typeof V156_STEPS)[number] | (typeof V155_STEPS)[number] | (typeof V157_STEPS)[number] | (typeof V158_STEPS)[number];
|
|
444
446
|
/**
|
|
445
447
|
* v148 (docs/plans/results-routing.md): every table the results-routing arc uses, landed at once because a current
|
|
446
448
|
* store runs no DDL. Slice 1 writes run_cohort, route_record and models_generation; route_rule, route_exposure* and
|
|
@@ -528,6 +530,8 @@ export declare const V153_STEPS: readonly ["plain-tables", "plain-columns"];
|
|
|
528
530
|
export declare const V154_STEPS: readonly ["check-stamps"];
|
|
529
531
|
export { V155_STEPS };
|
|
530
532
|
export { V156_STEPS };
|
|
533
|
+
export { V157_STEPS };
|
|
534
|
+
export { V158_STEPS };
|
|
531
535
|
/**
|
|
532
536
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
533
537
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -1188,6 +1192,10 @@ export type LeadTurn = {
|
|
|
1188
1192
|
tokensIn: number | null;
|
|
1189
1193
|
tokensOut: number | null;
|
|
1190
1194
|
failureReason: string | null;
|
|
1195
|
+
/** v158: why it failed, more exactly (lead-latency-schema.ts); null when unknown or before v158. */
|
|
1196
|
+
failureDetail?: LeadFailureDetail | null;
|
|
1197
|
+
/** v158: which model answered (default, the quick model, or the main model after a quick step asked for more). */
|
|
1198
|
+
route?: LeadRoute | null;
|
|
1191
1199
|
};
|
|
1192
1200
|
/** Who placed a hold — and therefore who alone may lift it. */
|
|
1193
1201
|
/** 'stop' (v52) = the pause one exact attempt's operator stop placed — lifted
|
|
@@ -4393,6 +4401,10 @@ export declare class Store {
|
|
|
4393
4401
|
reportedCostUsd?: number | null;
|
|
4394
4402
|
/** v141: how the request's own events said it was billed (a subscription harness); absent or null: unknown. */
|
|
4395
4403
|
billingReported?: "subscription" | "api-key" | null;
|
|
4404
|
+
/** v158: the tool names a lead step asked for (names only, never arguments); absent: not recorded. */
|
|
4405
|
+
tools?: readonly string[];
|
|
4406
|
+
/** v158: why it failed, more exactly than failureReason; absent: unknown. */
|
|
4407
|
+
failureDetail?: LeadFailureDetail | null;
|
|
4396
4408
|
}, now: Date): boolean;
|
|
4397
4409
|
/**
|
|
4398
4410
|
* The crash sweep: turns past their deadline become failed('crashed'),
|
|
@@ -6583,6 +6595,8 @@ export declare class Store {
|
|
|
6583
6595
|
tokensIn: number;
|
|
6584
6596
|
tokensOut: number;
|
|
6585
6597
|
failureReason?: string;
|
|
6598
|
+
/** v158: why it failed, more exactly than failureReason (the words a later reader shows); absent: unknown. */
|
|
6599
|
+
failureDetail?: LeadFailureDetail | null;
|
|
6586
6600
|
/** Answered turns: the assistant row, written in the same transaction as the promotion. A timed-out turn's notice. */
|
|
6587
6601
|
message?: {
|
|
6588
6602
|
text: string;
|
|
@@ -8521,3 +8535,18 @@ export declare function taskCheckStamp(store: Store, repo: string, taskId: strin
|
|
|
8521
8535
|
runId: number;
|
|
8522
8536
|
reading: StampReading;
|
|
8523
8537
|
} | null;
|
|
8538
|
+
/** A thread's newest lead turn, whatever its state; null before its first. What a surface reads back to say how the
|
|
8539
|
+
* last reply ended after a restart (docs/plans/responsive-chat.md §3.4): the saved row, never process memory. */
|
|
8540
|
+
export declare function latestLeadTurnOfThread(store: Store, thread: number): LeadTurn | null;
|
|
8541
|
+
/** Settings → Lead's "Quick answers use a faster model" (v158): absent reads off, the shipped default
|
|
8542
|
+
* (docs/plans/responsive-chat.md §8: on only once the lead-answer eval passes). */
|
|
8543
|
+
export declare function leadQuickAnswersOn(store: Store): boolean;
|
|
8544
|
+
/** Turn quick answers on or off, recording who and when (the caller proved the person may change the lead's settings). */
|
|
8545
|
+
export declare function setLeadQuickAnswers(store: Store, enabled: boolean, by: string, now: Date): void;
|
|
8546
|
+
/** Which model answers a running reply (v158): written at admission and when a quick step hands over to the main model.
|
|
8547
|
+
* Only the turn's own running generation may write it. */
|
|
8548
|
+
export declare function setLeadTurnRoute(store: Store, turn: number, generation: number, route: LeadRoute): boolean;
|
|
8549
|
+
/** Whether a new message in this thread may be the answer to the lead's own question (ask_owner): the thread's newest
|
|
8550
|
+
* reply asked one, less than LEAD_ASK_TTL_MS ago. Conservative on purpose: the quick-answer router (§8) keeps such a
|
|
8551
|
+
* message on the main model. */
|
|
8552
|
+
export declare function leadAskAwaitsAnswer(store: Store, thread: number, now: Date): boolean;
|
package/dist/store.js
CHANGED
|
@@ -13,6 +13,8 @@ import { codeMapRemoteChanged, codeMapShapeProblem, forgetCodeMap, V152_ADDED_CO
|
|
|
13
13
|
import { readStamps, stampDigest, stampRefusal, V154_SCHEMA } from "./check-stamp.js";
|
|
14
14
|
import { FINDING_SOURCES, FINDING_SOURCES_BEFORE_LEAD, V155_SCHEMA, V155_STEPS } from "./fleet-schema.js";
|
|
15
15
|
import { V156_SCHEMA, V156_STEPS } from "./fallback-schema.js";
|
|
16
|
+
import { V158_ADDED_COLUMNS, V158_SCHEMA, V158_STEPS, LEAD_FAILURE_DETAILS, LEAD_ROUTES } from "./lead-latency-schema.js";
|
|
17
|
+
import { V157_SCHEMA, V157_STEPS } from "./chat-live-schema.js";
|
|
16
18
|
import { refArchived, taskArchived } from "./task-archive.js";
|
|
17
19
|
export { refArchived, taskArchived } from "./task-archive.js";
|
|
18
20
|
import { CHAT_PROVIDERS, CHAT_SCHEMA } from "./contracts/chat-tables.js";
|
|
@@ -1142,7 +1144,11 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1142
1144
|
// v155 adds, and changes no saved row (docs/plans/fleet-follow-through.md): findings and their answers, check repairs,
|
|
1143
1145
|
// before-and-after evidence and provider captures (V155_SCHEMA, new tables only; fleet-schema.ts).
|
|
1144
1146
|
// v156 adds fallback chains (V156_SCHEMA, new tables only; fallback-schema.ts), after v155.
|
|
1145
|
-
|
|
1147
|
+
// v157 is the chat apps' live placeholders (chat-apps slice; merged separately).
|
|
1148
|
+
// v158 adds, and changes no saved row (docs/plans/responsive-chat.md, slice 5): the tools each lead step asked for, an
|
|
1149
|
+
// exact failure detail on a step and a reply, which model answered a reply (mate_turn.route), and the quick-answer
|
|
1150
|
+
// switch (lead_quick_setting). V158_SCHEMA, V158_ADDED_COLUMNS (lead-latency-schema.ts): saved rows read NULL.
|
|
1151
|
+
export const SCHEMA_VERSION = 158;
|
|
1146
1152
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1147
1153
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1148
1154
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1275,7 +1281,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1275
1281
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1276
1282
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1277
1283
|
*/
|
|
1278
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156]);
|
|
1284
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158]);
|
|
1279
1285
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1280
1286
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1281
1287
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1936,6 +1942,8 @@ export const V153_STEPS = Object.freeze(["plain-tables", "plain-columns"]);
|
|
|
1936
1942
|
export const V154_STEPS = Object.freeze(["check-stamps"]);
|
|
1937
1943
|
export { V155_STEPS };
|
|
1938
1944
|
export { V156_STEPS };
|
|
1945
|
+
export { V157_STEPS };
|
|
1946
|
+
export { V158_STEPS };
|
|
1939
1947
|
/**
|
|
1940
1948
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1941
1949
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -5274,6 +5282,17 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
5274
5282
|
// v156: fallback chains (new tables only). Idempotent, as v155's steps are.
|
|
5275
5283
|
db.exec(V156_SCHEMA);
|
|
5276
5284
|
step("fallback-tables");
|
|
5285
|
+
// v157: live replies in Slack, Discord and Teams (new tables only). Idempotent, as v156's step is.
|
|
5286
|
+
db.exec(V157_SCHEMA);
|
|
5287
|
+
step("chat-live-tables");
|
|
5288
|
+
// v158: the lead's latency records and its quick-answer switch (V158_SCHEMA, V158_ADDED_COLUMNS: saved rows read
|
|
5289
|
+
// NULL). Idempotent, as v156's and v157's steps are.
|
|
5290
|
+
db.exec(V158_SCHEMA);
|
|
5291
|
+
step("lead-quick-setting");
|
|
5292
|
+
for (const [table, columns] of Object.entries(V158_ADDED_COLUMNS))
|
|
5293
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
5294
|
+
addColumn(db, table, column, definition);
|
|
5295
|
+
step("lead-latency-columns");
|
|
5277
5296
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
5278
5297
|
// nothing to reclaim.
|
|
5279
5298
|
if (!fresh)
|
|
@@ -13210,9 +13229,9 @@ export class Store {
|
|
|
13210
13229
|
const changed = this.db
|
|
13211
13230
|
.prepare(`UPDATE chat_turn SET state = ?, failure_reason = ?, tokens_in = ?, tokens_out = ?,
|
|
13212
13231
|
settled_microusd = ?, unknown_spend = ?, reply_bytes = ?, candidate_count = ?, cache_read_tokens = ?, cache_write_tokens = ?,
|
|
13213
|
-
model_reported = ?, provider_turns = ?, reported_cost_usd = ?, billing_reported = ?, finished_at = ?
|
|
13232
|
+
model_reported = ?, provider_turns = ?, reported_cost_usd = ?, billing_reported = ?, tools = ?, failure_detail = ?, finished_at = ?
|
|
13214
13233
|
WHERE id = ? AND generation = ? AND state IN ('queued','running')`)
|
|
13215
|
-
.run(outcome.state, outcome.failureReason ?? null, outcome.tokensIn ?? null, outcome.tokensOut ?? null, outcome.settledMicrousd, outcome.unknownSpend === true ? 1 : 0, outcome.replyBytes ?? null, outcome.candidateCount ?? null, outcome.cacheReadTokens ?? null, outcome.cacheWriteTokens ?? null, reportedModelOf(outcome.modelReported), providerTurnsOf(outcome.providerTurns), reportedCostUsdOf(outcome.reportedCostUsd), outcome.billingReported === "subscription" || outcome.billingReported === "api-key" ? outcome.billingReported : null, now.toISOString(), id, generation);
|
|
13234
|
+
.run(outcome.state, outcome.failureReason ?? null, outcome.tokensIn ?? null, outcome.tokensOut ?? null, outcome.settledMicrousd, outcome.unknownSpend === true ? 1 : 0, outcome.replyBytes ?? null, outcome.candidateCount ?? null, outcome.cacheReadTokens ?? null, outcome.cacheWriteTokens ?? null, reportedModelOf(outcome.modelReported), providerTurnsOf(outcome.providerTurns), reportedCostUsdOf(outcome.reportedCostUsd), outcome.billingReported === "subscription" || outcome.billingReported === "api-key" ? outcome.billingReported : null, outcome.tools === undefined ? null : JSON.stringify(outcome.tools.filter(name => /^[a-z_]{1,64}$/.test(name)).slice(0, 32)), outcome.failureDetail != null && LEAD_FAILURE_DETAILS.includes(outcome.failureDetail) ? outcome.failureDetail : null, now.toISOString(), id, generation);
|
|
13216
13235
|
return Number(changed.changes) === 1;
|
|
13217
13236
|
}
|
|
13218
13237
|
/**
|
|
@@ -18016,9 +18035,9 @@ export class Store {
|
|
|
18016
18035
|
return false;
|
|
18017
18036
|
const settled = outcome.unknownSpend === true ? Math.max(Number(before["reserved"]), outcome.settledMicrousd) : outcome.settledMicrousd;
|
|
18018
18037
|
const changed = this.db
|
|
18019
|
-
.prepare(`UPDATE mate_turn SET state = ?, settled_microusd = ?, tokens_in = ?, tokens_out = ?, failure_reason = ?, finished_at = ?
|
|
18038
|
+
.prepare(`UPDATE mate_turn SET state = ?, settled_microusd = ?, tokens_in = ?, tokens_out = ?, failure_reason = ?, failure_detail = ?, finished_at = ?
|
|
18020
18039
|
WHERE id = ? AND generation = ? AND state IN ('queued','running')`)
|
|
18021
|
-
.run(outcome.state, settled, outcome.tokensIn, outcome.tokensOut, outcome.failureReason ?? null, now.toISOString(), id, generation);
|
|
18040
|
+
.run(outcome.state, settled, outcome.tokensIn, outcome.tokensOut, outcome.failureReason ?? null, outcome.failureDetail != null && LEAD_FAILURE_DETAILS.includes(outcome.failureDetail) ? outcome.failureDetail : null, now.toISOString(), id, generation);
|
|
18022
18041
|
if (Number(changed.changes) !== 1)
|
|
18023
18042
|
return false;
|
|
18024
18043
|
this.db.prepare("UPDATE lead_session SET spent_microusd = spent_microusd + ? WHERE id = ?").run(settled, Number(before["session"]));
|
|
@@ -21235,6 +21254,8 @@ function readLeadTurn(row) {
|
|
|
21235
21254
|
tokensIn: maybeN("tokens_in"),
|
|
21236
21255
|
tokensOut: maybeN("tokens_out"),
|
|
21237
21256
|
failureReason: maybe("failure_reason"),
|
|
21257
|
+
failureDetail: LEAD_FAILURE_DETAILS.includes(row["failure_detail"]) ? row["failure_detail"] : null,
|
|
21258
|
+
route: LEAD_ROUTES.includes(row["route"]) ? row["route"] : null,
|
|
21238
21259
|
};
|
|
21239
21260
|
}
|
|
21240
21261
|
function readVerifyCommand(row) {
|
|
@@ -23173,3 +23194,35 @@ function widenFindingSources(db) {
|
|
|
23173
23194
|
const columns = db.prepare("PRAGMA table_info(finding)").all().map(one => String(one["name"]));
|
|
23174
23195
|
rebuildExact(db, "finding", named(stored), named(stored.replace(FINDING_SOURCES_BEFORE_LEAD, FINDING_SOURCES)), columns);
|
|
23175
23196
|
}
|
|
23197
|
+
/** A thread's newest lead turn, whatever its state; null before its first. What a surface reads back to say how the
|
|
23198
|
+
* last reply ended after a restart (docs/plans/responsive-chat.md §3.4): the saved row, never process memory. */
|
|
23199
|
+
export function latestLeadTurnOfThread(store, thread) {
|
|
23200
|
+
const row = store.handle.prepare("SELECT * FROM mate_turn WHERE thread = ? ORDER BY id DESC LIMIT 1").get(thread);
|
|
23201
|
+
return row === undefined ? null : readLeadTurn(row);
|
|
23202
|
+
}
|
|
23203
|
+
/** Settings → Lead's "Quick answers use a faster model" (v158): absent reads off, the shipped default
|
|
23204
|
+
* (docs/plans/responsive-chat.md §8: on only once the lead-answer eval passes). */
|
|
23205
|
+
export function leadQuickAnswersOn(store) {
|
|
23206
|
+
const row = store.handle.prepare("SELECT enabled FROM lead_quick_setting WHERE id = 1").get();
|
|
23207
|
+
return row !== undefined && Number(row["enabled"]) === 1;
|
|
23208
|
+
}
|
|
23209
|
+
/** Turn quick answers on or off, recording who and when (the caller proved the person may change the lead's settings). */
|
|
23210
|
+
export function setLeadQuickAnswers(store, enabled, by, now) {
|
|
23211
|
+
store.handle.prepare(`INSERT INTO lead_quick_setting (id, enabled, updated_by, updated_at) VALUES (1, ?, ?, ?)
|
|
23212
|
+
ON CONFLICT(id) DO UPDATE SET enabled = excluded.enabled, updated_by = excluded.updated_by, updated_at = excluded.updated_at`).run(enabled ? 1 : 0, by, now.toISOString());
|
|
23213
|
+
}
|
|
23214
|
+
/** Which model answers a running reply (v158): written at admission and when a quick step hands over to the main model.
|
|
23215
|
+
* Only the turn's own running generation may write it. */
|
|
23216
|
+
export function setLeadTurnRoute(store, turn, generation, route) {
|
|
23217
|
+
if (!LEAD_ROUTES.includes(route))
|
|
23218
|
+
return false;
|
|
23219
|
+
return Number(store.handle.prepare("UPDATE mate_turn SET route = ? WHERE id = ? AND generation = ? AND state = 'running'").run(route, turn, generation).changes) === 1;
|
|
23220
|
+
}
|
|
23221
|
+
/** Whether a new message in this thread may be the answer to the lead's own question (ask_owner): the thread's newest
|
|
23222
|
+
* reply asked one, less than LEAD_ASK_TTL_MS ago. Conservative on purpose: the quick-answer router (§8) keeps such a
|
|
23223
|
+
* message on the main model. */
|
|
23224
|
+
export function leadAskAwaitsAnswer(store, thread, now) {
|
|
23225
|
+
const row = store.handle.prepare(`SELECT a.created_at AS at FROM lead_message m JOIN lead_ask a ON a.turn = m.turn JOIN mate_turn t ON t.id = m.turn
|
|
23226
|
+
WHERE m.id = (SELECT MAX(id) FROM lead_message WHERE thread = ? AND role = 'assistant') AND t.state = 'answered'`).get(thread);
|
|
23227
|
+
return row !== undefined && Date.parse(String(row["at"])) + LEAD_ASK_TTL_MS > now.getTime();
|
|
23228
|
+
}
|
|
@@ -27,6 +27,7 @@ export type SubscriptionLeadRunner = (request: SubscriptionLeadRequest) => Promi
|
|
|
27
27
|
} | {
|
|
28
28
|
ok: false;
|
|
29
29
|
problem: string;
|
|
30
|
+
quiet?: boolean;
|
|
30
31
|
}>;
|
|
31
32
|
/** The model-visible request. Tool results are already redacted by leadView. */
|
|
32
33
|
export declare function composeSubscriptionLeadPrompt(request: Omit<SubscriptionLeadRequest, "provider" | "model" | "timeoutMs" | "signal">): string;
|
|
@@ -49,4 +50,5 @@ export declare function performSubscriptionLeadRequest(request: SubscriptionLead
|
|
|
49
50
|
} | {
|
|
50
51
|
ok: false;
|
|
51
52
|
problem: string;
|
|
53
|
+
quiet?: boolean;
|
|
52
54
|
}>;
|
|
@@ -260,8 +260,10 @@ export async function performSubscriptionLeadRequest(request, runner = run) {
|
|
|
260
260
|
processGroup: true,
|
|
261
261
|
...(streaming ? { onStdout: claudeStreamReader(request.onText) } : {}),
|
|
262
262
|
});
|
|
263
|
+
// Whether the harness wrote anything at all before its deadline (v158, responsive-chat.md §7.5): a silent one never
|
|
264
|
+
// started answering; one with output was answering slowly. Recorded, never shown.
|
|
263
265
|
if (result.timedOut)
|
|
264
|
-
return { ok: false, problem: "timeout" };
|
|
266
|
+
return { ok: false, problem: "timeout", quiet: result.stdout.trim() === "" };
|
|
265
267
|
if (result.notFound)
|
|
266
268
|
return { ok: false, problem: "not-found" };
|
|
267
269
|
// v105: a streamed Claude turn says its plan's usage windows, and (once it answered) how this computer's Claude bills
|
package/dist/teams-chat.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { ChatState, type ChatIdentity } from "./chat-delivery-state.js";
|
|
1
|
+
import { ChatState, type ChatEvent, type ChatIdentity } from "./chat-delivery-state.js";
|
|
2
|
+
import { type ChatAckHooks, type ChatLiveTransport, type LiveReplies } from "./chat-live.js";
|
|
2
3
|
import { type ChatDeliveryOptions } from "./chat-delivery.js";
|
|
3
4
|
import { type TeamsApi } from "./teams-api.js";
|
|
4
|
-
export type TeamsChatOptions = Omit<ChatDeliveryOptions, "state" | "label" | "member" | "partSize" | "maxProposal"> & {
|
|
5
|
+
export type TeamsChatOptions = Omit<ChatDeliveryOptions, "state" | "label" | "member" | "partSize" | "maxProposal" | "live"> & {
|
|
5
6
|
api: TeamsApi;
|
|
7
|
+
/** The live "Working on it…" message (chat-live.ts): on in the worker. */
|
|
8
|
+
liveReplies?: LiveReplies;
|
|
6
9
|
};
|
|
7
10
|
export declare const teamsUserId: (v: unknown) => v is string;
|
|
8
11
|
/** A conversation id; a channel post's carries its thread as ";messageid=<first message>" (v89). */
|
|
@@ -27,3 +30,7 @@ export declare function teamsCard(text: string, actions: Record<string, unknown>
|
|
|
27
30
|
export declare function teamsMessages(text: string, markdown: boolean, last: (text: string) => Record<string, unknown>): Record<string, unknown>[];
|
|
28
31
|
/** Send the next pending part: a plain message, an edit, or a card with its tokens. */
|
|
29
32
|
export declare function deliverTeamsPart(options: TeamsChatOptions): Promise<boolean>;
|
|
33
|
+
/** The live message for one owner message: a plain Markdown activity, replaced whole (PUT) on every edit. */
|
|
34
|
+
export declare function teamsLive(options: Pick<TeamsChatOptions, "api" | "identity">, state: ChatState, event: ChatEvent, spacingMs?: number): ChatLiveTransport | null;
|
|
35
|
+
/** Teams gives bots no reactions: the arrival ack is "typing…", with nothing to take off. */
|
|
36
|
+
export declare function teamsAckHooks(options: Pick<TeamsChatOptions, "api" | "identity">, state: ChatState): ChatAckHooks;
|
package/dist/teams-chat.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resultShotsPruned } from "./result-shots.js";
|
|
|
4
4
|
import { chatQuestionButtons } from "./subagent-question.js";
|
|
5
5
|
import { chatAskButtons } from "./chat-ask.js";
|
|
6
6
|
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
7
|
+
import { isLiveMessage, liveSpacingOf, underCooldown } from "./chat-live.js";
|
|
7
8
|
import { channelAccess, chatObject as object, heldForWatch, planChatNotifications, planRoomMessages, processChatEvent, splitChatText } from "./chat-delivery.js";
|
|
8
9
|
import { PLATFORM_LIMITS } from "./text-limits.js";
|
|
9
10
|
import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
|
|
@@ -14,6 +15,9 @@ import { admitChatEvent } from "./chat-admission.js";
|
|
|
14
15
|
import { chatResultHref } from "./chat-controls.js";
|
|
15
16
|
import { TeamsError } from "./teams-api.js";
|
|
16
17
|
import { readTeamsSubmit } from "./contracts/teams-callback.js";
|
|
18
|
+
import { readChatPart } from "./contracts/chat-content.js";
|
|
19
|
+
const TEAMS_GONE = "Teams refused the request (404)";
|
|
20
|
+
const TEAMS_SLOW = "Teams asked to slow down";
|
|
17
21
|
const serviceKey = (conversation) => `serviceUrl:${conversation}`;
|
|
18
22
|
export const teamsUserId = (v) => typeof v === "string" && /^29:[A-Za-z0-9_=-]{10,200}$/.test(v);
|
|
19
23
|
/** A conversation id; a channel post's carries its thread as ";messageid=<first message>" (v89). */
|
|
@@ -43,6 +47,7 @@ export async function teamsMember(api, state, installation, member, channel) {
|
|
|
43
47
|
return false;
|
|
44
48
|
}
|
|
45
49
|
}
|
|
50
|
+
const teamsRateLimited = (error) => error instanceof TeamsError && error.code === TEAMS_SLOW ? error.retryMs : null;
|
|
46
51
|
export const teamsDelivery = (options) => {
|
|
47
52
|
const state = new ChatState(options.store, "teams");
|
|
48
53
|
return {
|
|
@@ -52,12 +57,13 @@ export const teamsDelivery = (options) => {
|
|
|
52
57
|
member: (member, channel) => teamsMember(options.api, state, options.identity.installation, member, channel),
|
|
53
58
|
// "Typing…" while the lead works; Teams gives bots no reactions, so there is no 👍 here.
|
|
54
59
|
warm: (event) => ({
|
|
55
|
-
typing: async () => {
|
|
60
|
+
typing: underCooldown(state, options.identity.installation, teamsRateLimited, () => options.clock?.() ?? new Date(), async () => {
|
|
56
61
|
const serviceUrl = state.meta(options.identity.installation, serviceKey(event.channel));
|
|
57
62
|
if (serviceUrl !== null)
|
|
58
63
|
await options.api("POST", serviceUrl, `/v3/conversations/${encodeURIComponent(event.channel)}/activities`, { type: "typing" });
|
|
59
|
-
},
|
|
64
|
+
}),
|
|
60
65
|
}),
|
|
66
|
+
...(options.liveReplies ? { live: (event) => teamsLive(options, state, event, liveSpacingOf(options.liveReplies)) } : {}),
|
|
61
67
|
partSize: 3500,
|
|
62
68
|
maxProposal: 6000,
|
|
63
69
|
};
|
|
@@ -246,8 +252,50 @@ export async function deliverTeamsPart(options) {
|
|
|
246
252
|
state.dropPart(row.id, error.message);
|
|
247
253
|
return true;
|
|
248
254
|
}
|
|
255
|
+
// The live message the reply was to replace is gone: the reply goes out as a new message.
|
|
256
|
+
const saved = readChatPart(row.payload), content = saved.ok ? saved.value : null;
|
|
257
|
+
if (content?.edit !== undefined && error instanceof TeamsError && error.code === TEAMS_GONE && isLiveMessage(state, row.event, content.edit)) {
|
|
258
|
+
const { edit: _edit, ...asNew } = content;
|
|
259
|
+
state.repaint(row.id, asNew);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
249
262
|
const problem = error instanceof TeamsError ? error : new TeamsError("Teams delivery failed", 15_000, true);
|
|
250
263
|
state.partFailed(row.id, { problem: problem.message, until: new Date(now.getTime() + problem.retryMs).toISOString(), uncertain: problem.uncertain }, 20);
|
|
251
264
|
return true;
|
|
252
265
|
}
|
|
253
266
|
}
|
|
267
|
+
/** The live message for one owner message: a plain Markdown activity, replaced whole (PUT) on every edit. */
|
|
268
|
+
export function teamsLive(options, state, event, spacingMs) {
|
|
269
|
+
const serviceUrl = state.meta(options.identity.installation, serviceKey(event.channel));
|
|
270
|
+
if (serviceUrl === null)
|
|
271
|
+
return null;
|
|
272
|
+
const activities = `/v3/conversations/${encodeURIComponent(event.channel)}/activities`;
|
|
273
|
+
const body = (text) => ({ type: "message", text, textFormat: "markdown" });
|
|
274
|
+
return {
|
|
275
|
+
platform: "teams",
|
|
276
|
+
...(spacingMs === undefined ? {} : { spacingMs }),
|
|
277
|
+
limit: PLATFORM_LIMITS.teams - 200,
|
|
278
|
+
create: async (text) => {
|
|
279
|
+
const answer = await options.api("POST", serviceUrl, activities, body(text));
|
|
280
|
+
if (typeof answer.id !== "string" || answer.id === "")
|
|
281
|
+
throw new TeamsError("Teams did not confirm the message", 15_000, true);
|
|
282
|
+
return answer.id;
|
|
283
|
+
},
|
|
284
|
+
edit: async (message, text) => { await options.api("PUT", serviceUrl, `${activities}/${encodeURIComponent(message)}`, body(text)); },
|
|
285
|
+
rateLimited: error => error instanceof TeamsError && error.code === TEAMS_SLOW ? error.retryMs : null,
|
|
286
|
+
uncertain: error => !(error instanceof TeamsError) || error.uncertain,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/** Teams gives bots no reactions: the arrival ack is "typing…", with nothing to take off. */
|
|
290
|
+
export function teamsAckHooks(options, state) {
|
|
291
|
+
return {
|
|
292
|
+
typing: async (event) => {
|
|
293
|
+
const serviceUrl = state.meta(options.identity.installation, serviceKey(event.channel));
|
|
294
|
+
if (serviceUrl !== null)
|
|
295
|
+
await options.api("POST", serviceUrl, `/v3/conversations/${encodeURIComponent(event.channel)}/activities`, { type: "typing" });
|
|
296
|
+
},
|
|
297
|
+
refused: () => false,
|
|
298
|
+
settled: () => false,
|
|
299
|
+
rateLimited: error => error instanceof TeamsError && error.code === TEAMS_SLOW ? error.retryMs : null,
|
|
300
|
+
};
|
|
301
|
+
}
|
package/dist/teams.js
CHANGED
|
@@ -8,7 +8,8 @@ import { abortablePause, workChatQueue } from "./chat-follow.js";
|
|
|
8
8
|
import { ChatState } from "./chat-delivery-state.js";
|
|
9
9
|
import { chatObject as object } from "./chat-delivery.js";
|
|
10
10
|
import { loadTeamsCredentials, teamsAccessToken, teamsApi, verifyTeamsToken, TeamsError } from "./teams-api.js";
|
|
11
|
-
import { deliverTeamsPart, planTeamsNotifications, planTeamsRooms, processTeamsEvent, receiveTeams } from "./teams-chat.js";
|
|
11
|
+
import { deliverTeamsPart, planTeamsNotifications, planTeamsRooms, processTeamsEvent, receiveTeams, teamsAckHooks } from "./teams-chat.js";
|
|
12
|
+
import { runChatAcks } from "./chat-live.js";
|
|
12
13
|
import { limitWords } from "./request-budget.js";
|
|
13
14
|
export const TEAMS_ACTIVITY_BYTES = 64 * 1024;
|
|
14
15
|
export const TEAMS_MESSAGES_PATH = "/teams/messages";
|
|
@@ -95,7 +96,7 @@ export async function followTeams(options) {
|
|
|
95
96
|
options.signal.addEventListener("abort", stop, { once: true });
|
|
96
97
|
const heartbeat = setInterval(() => { if (!same() || !state.lease(credentials.installation, owner, new Date()))
|
|
97
98
|
stop(); }, 10_000);
|
|
98
|
-
const chat = { ...options, identity: credentials, owner, api: teamsApi(credentials, options.fetcher), canNotify: options.notifications, current: same };
|
|
99
|
+
const chat = { ...options, identity: credentials, owner, api: teamsApi(credentials, options.fetcher), canNotify: options.notifications, current: same, liveReplies: true };
|
|
99
100
|
try {
|
|
100
101
|
// Connected means the app can sign in; inbound reachability is the operator's endpoint to verify.
|
|
101
102
|
try {
|
|
@@ -107,6 +108,8 @@ export async function followTeams(options) {
|
|
|
107
108
|
await pause(30_000);
|
|
108
109
|
continue;
|
|
109
110
|
}
|
|
111
|
+
// "typing…" on each owner message as it arrives (the receiver saves it, maybe in another process), beside the queue.
|
|
112
|
+
runChatAcks({ state, installation: credentials.installation, owner, current: same, hooks: teamsAckHooks(chat, state), same, signal: options.signal });
|
|
110
113
|
await workChatQueue({
|
|
111
114
|
same,
|
|
112
115
|
ready: () => !(state.retryAt(credentials.installation) > new Date().toISOString()),
|
package/dist/telegram-lead.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type PhoneLink } from "./chat-channel.js";
|
|
|
2
2
|
export { channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, tooLongText, whichTaskText, replyContextFor, NO_PHONE_LINK, NO_TASK_LINK, proposalLink, confirmedLink, proposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText, CHAT_ACTION_PARITY as TELEGRAM_ACTION_PARITY, parityGaps, type PhoneLink, type ResolvedLead, type ParitySupport } from "./chat-channel.js";
|
|
3
3
|
import { type LeadAsk, type Store, type TelegramBinding } from "./store.js";
|
|
4
4
|
import type { SubscriptionLeadRunner } from "./subscription-chat.js";
|
|
5
|
+
import { type TelegramLiveTiming } from "./telegram-live.js";
|
|
5
6
|
import type { TelegramTransport } from "./telegram.js";
|
|
6
7
|
import { type TelegramCallbackButton, type TelegramUpdate } from "./contracts/telegram-callback.js";
|
|
7
8
|
/** How long one claimed turn may go without a heartbeat before another poller may take it over. */
|
|
@@ -39,6 +40,8 @@ export type TelegramConversationOptions = {
|
|
|
39
40
|
ok: false;
|
|
40
41
|
message: string;
|
|
41
42
|
}>;
|
|
43
|
+
/** Injected by tests: the live reply's timings (telegram-live.ts); production uses its defaults. */
|
|
44
|
+
liveTiming?: TelegramLiveTiming;
|
|
42
45
|
};
|
|
43
46
|
/** One Bot API call, the shape telegram.ts injects. */
|
|
44
47
|
export type Transport = TelegramTransport;
|