toolroll 0.9.36 → 0.9.37
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 +58 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +7 -2
- package/dist/contracts/contract.js +21 -12
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +5 -0
- package/dist/contracts/flow.js +5 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/desktop-update.d.ts +2 -2
- package/dist/discord-chat.js +31 -20
- package/dist/flow-engine.js +14 -17
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.js +7 -1
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-steps.js +3 -2
- package/dist/flows.d.ts +2 -4
- package/dist/flows.js +9 -5
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +108 -275
- package/dist/mcp-connect.d.ts +24 -4
- package/dist/mcp-connect.js +78 -15
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +9 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/task-review-brief.d.ts +1 -7
- package/dist/task-review-brief.js +6 -6
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +142 -1
- package/dist/text-limits.js +144 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/package.json +1 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type Store } from "./store.js";
|
|
2
|
+
import { chatSchema, chatTables } from "./contracts/chat-tables.js";
|
|
3
|
+
import { savedChatPart, type ChatContent, type ChatEventBody } from "./contracts/chat-content.js";
|
|
4
|
+
export { savedChatPart, type ChatContent, type ChatEventBody };
|
|
5
|
+
export { chatSchema, chatTables };
|
|
2
6
|
export type ChatIdentity = {
|
|
3
7
|
installation: string;
|
|
4
8
|
team?: string;
|
|
@@ -57,72 +61,6 @@ export type ChatRoom = {
|
|
|
57
61
|
binding: number;
|
|
58
62
|
cursor: number;
|
|
59
63
|
};
|
|
60
|
-
export type ChatContent = {
|
|
61
|
-
text: string;
|
|
62
|
-
/** Where the part is sent when not the binding's own DM: a room's channel. */
|
|
63
|
-
channel?: string;
|
|
64
|
-
proposal?: number;
|
|
65
|
-
image?: {
|
|
66
|
-
taskId: string;
|
|
67
|
-
run: number;
|
|
68
|
-
artifact: number;
|
|
69
|
-
sha256: string;
|
|
70
|
-
};
|
|
71
|
-
/** A screenshot sent with a result (result-shots.ts): it follows that result's message part, in its thread when
|
|
72
|
-
* the app allows; a refused upload becomes one plain line, and one removed by retention goes quietly. */
|
|
73
|
-
shot?: {
|
|
74
|
-
follows: number | null;
|
|
75
|
-
};
|
|
76
|
-
task?: string;
|
|
77
|
-
run?: number;
|
|
78
|
-
edit?: string;
|
|
79
|
-
phase?: "armed";
|
|
80
|
-
link?: {
|
|
81
|
-
label: string;
|
|
82
|
-
path: string;
|
|
83
|
-
};
|
|
84
|
-
/** More link buttons after `link`: [Look first] beside [Merge] or [Accept and finish]. */
|
|
85
|
-
also?: Array<{
|
|
86
|
-
label: string;
|
|
87
|
-
path: string;
|
|
88
|
-
}>;
|
|
89
|
-
/** A flow decision's buttons ride this part (v88): minted when it is planned. */
|
|
90
|
-
flow?: {
|
|
91
|
-
card: number;
|
|
92
|
-
entry: number;
|
|
93
|
-
actions: Array<"approve" | "edit" | "send-back">;
|
|
94
|
-
};
|
|
95
|
-
/** A flow's "Person chooses" buttons ride this part: minted when it is planned. */
|
|
96
|
-
choose?: {
|
|
97
|
-
card: number;
|
|
98
|
-
entry: number;
|
|
99
|
-
options: Array<{
|
|
100
|
-
choice: number;
|
|
101
|
-
label: string;
|
|
102
|
-
}>;
|
|
103
|
-
};
|
|
104
|
-
/** "Use this as your note?" Yes / No about the person's message `held` (an event id), for one choice's visit. */
|
|
105
|
-
note?: {
|
|
106
|
-
card: number;
|
|
107
|
-
entry: number;
|
|
108
|
-
held: string;
|
|
109
|
-
};
|
|
110
|
-
/** A teammate's question's buttons ride this part (v93): each option, then one to answer in words (choice null). */
|
|
111
|
-
question?: {
|
|
112
|
-
id: number;
|
|
113
|
-
choices: Array<{
|
|
114
|
-
choice: string | null;
|
|
115
|
-
label: string;
|
|
116
|
-
}>;
|
|
117
|
-
};
|
|
118
|
-
/** The lead's question to its owner rides this part: its options, then "Something else". */
|
|
119
|
-
ask?: {
|
|
120
|
-
turn: number;
|
|
121
|
-
options: string[];
|
|
122
|
-
};
|
|
123
|
-
/** The lead's own reply, already shaped (reply-shape.ts): the channel renders its bold anchors and labelled links in its own format. */
|
|
124
|
-
voice?: true;
|
|
125
|
-
};
|
|
126
64
|
export declare const chatHash: (text: string) => string;
|
|
127
65
|
export declare class ChatState {
|
|
128
66
|
readonly store: Store;
|
|
@@ -170,7 +108,10 @@ export declare class ChatState {
|
|
|
170
108
|
revoke(installation: string, now?: Date): void;
|
|
171
109
|
pairing(installation: string, approver: string, generation: number, now?: Date): string;
|
|
172
110
|
pair(identity: ChatIdentity, hash: string, member: string, channel: string, now: Date): ChatBinding | null;
|
|
173
|
-
|
|
111
|
+
/** Save a received event with its body (contracts/chat-content.ts), versioned. */
|
|
112
|
+
enqueue(event: Omit<ChatEvent, "session" | "state" | "next_at" | "payload"> & {
|
|
113
|
+
payload: ChatEventBody;
|
|
114
|
+
}): boolean;
|
|
174
115
|
event(id: string): ChatEvent | null;
|
|
175
116
|
next(installation: string, now: Date): ChatEvent | null;
|
|
176
117
|
defer(event: string, problem: string, until: Date): void;
|
|
@@ -181,11 +122,12 @@ export declare class ChatState {
|
|
|
181
122
|
lease(installation: string, owner: string, now: Date): boolean;
|
|
182
123
|
owns(installation: string, owner: string, now?: Date): boolean;
|
|
183
124
|
}
|
|
184
|
-
|
|
185
|
-
export declare
|
|
125
|
+
/** A saved part's content. One that can't be read is a delivery problem that names the field, never a guess. */
|
|
126
|
+
export declare function partContent(payload: string): ChatContent;
|
|
186
127
|
export declare class ChatDeliveryError extends Error {
|
|
187
128
|
readonly code: string;
|
|
188
129
|
readonly retryMs: number;
|
|
189
130
|
readonly uncertain: boolean;
|
|
190
|
-
|
|
131
|
+
readonly permanent: boolean;
|
|
132
|
+
constructor(code: string, retryMs?: number, uncertain?: boolean, permanent?: boolean);
|
|
191
133
|
}
|
|
@@ -1,136 +1,10 @@
|
|
|
1
1
|
/** Durable private-chat receipts. Each transport has its own tables and lease. */
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { MATE_ASK_TTL_MS } from "./store.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
approver TEXT NOT NULL, generation INTEGER NOT NULL, created TEXT NOT NULL, revoked TEXT
|
|
9
|
-
);
|
|
10
|
-
CREATE UNIQUE INDEX IF NOT EXISTS chat_one_binding_member ON chat_binding(installation, member) WHERE revoked IS NULL;
|
|
11
|
-
CREATE TABLE IF NOT EXISTS chat_pair (
|
|
12
|
-
hash TEXT PRIMARY KEY, installation TEXT NOT NULL, approver TEXT NOT NULL,
|
|
13
|
-
generation INTEGER NOT NULL, expires TEXT NOT NULL, consumed TEXT
|
|
14
|
-
);
|
|
15
|
-
CREATE TABLE IF NOT EXISTS chat_event (
|
|
16
|
-
id TEXT PRIMARY KEY, installation TEXT NOT NULL, binding INTEGER REFERENCES chat_binding(id),
|
|
17
|
-
kind TEXT NOT NULL CHECK(kind IN ('message','action','pair','notice')),
|
|
18
|
-
channel TEXT NOT NULL, member TEXT NOT NULL, ts TEXT NOT NULL, thread TEXT NOT NULL,
|
|
19
|
-
payload TEXT NOT NULL, created TEXT NOT NULL,
|
|
20
|
-
session INTEGER REFERENCES mate_session(id), state TEXT NOT NULL DEFAULT 'queued' CHECK(state IN ('queued','done','dropped')),
|
|
21
|
-
next_at TEXT, problem TEXT
|
|
22
|
-
);
|
|
23
|
-
CREATE INDEX IF NOT EXISTS chat_pending_event ON chat_event(installation,state,next_at);
|
|
24
|
-
CREATE TABLE IF NOT EXISTS chat_part (
|
|
25
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, event TEXT NOT NULL REFERENCES chat_event(id), ordinal INTEGER NOT NULL,
|
|
26
|
-
payload TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','sent','dropped')),
|
|
27
|
-
message TEXT, file TEXT, uploaded INTEGER NOT NULL DEFAULT 0, attempts INTEGER NOT NULL DEFAULT 0,
|
|
28
|
-
uncertain INTEGER NOT NULL DEFAULT 0, next_at TEXT, problem TEXT, created TEXT NOT NULL,
|
|
29
|
-
UNIQUE(event,ordinal)
|
|
30
|
-
);
|
|
31
|
-
CREATE TABLE IF NOT EXISTS chat_action (
|
|
32
|
-
token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
|
|
33
|
-
proposal INTEGER NOT NULL REFERENCES mate_proposal(id), phase TEXT NOT NULL CHECK(phase IN ('confirm','dismiss','yes','cancel')),
|
|
34
|
-
expires TEXT NOT NULL, consumed TEXT
|
|
35
|
-
);
|
|
36
|
-
CREATE TABLE IF NOT EXISTS chat_progress (
|
|
37
|
-
binding INTEGER NOT NULL REFERENCES chat_binding(id), run INTEGER NOT NULL REFERENCES run(id),
|
|
38
|
-
part INTEGER NOT NULL REFERENCES chat_part(id), digest TEXT NOT NULL,
|
|
39
|
-
PRIMARY KEY(binding,run)
|
|
40
|
-
);
|
|
41
|
-
CREATE TABLE IF NOT EXISTS chat_runtime (
|
|
42
|
-
installation TEXT PRIMARY KEY, owner TEXT, lease_until TEXT, connected TEXT,
|
|
43
|
-
problem TEXT, retry_at TEXT, notification INTEGER NOT NULL DEFAULT 0
|
|
44
|
-
);
|
|
45
|
-
`;
|
|
46
|
-
/** v73: a chat's place in the shared team conversations — a channel follows one
|
|
47
|
-
* conversation, a DM may select one. Rows are revoked, never deleted. */
|
|
48
|
-
const CHAT_ROOM_SCHEMA = `
|
|
49
|
-
CREATE TABLE IF NOT EXISTS chat_room (
|
|
50
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, installation TEXT NOT NULL, chat TEXT NOT NULL,
|
|
51
|
-
kind TEXT NOT NULL CHECK(kind IN ('group','private')), conversation TEXT NOT NULL,
|
|
52
|
-
binding INTEGER NOT NULL REFERENCES chat_binding(id),
|
|
53
|
-
bound_by TEXT NOT NULL, bound TEXT NOT NULL, cursor INTEGER NOT NULL DEFAULT 0,
|
|
54
|
-
revoked TEXT, revoked_by TEXT
|
|
55
|
-
);
|
|
56
|
-
CREATE UNIQUE INDEX IF NOT EXISTS chat_room_live ON chat_room(installation, chat) WHERE revoked IS NULL;
|
|
57
|
-
CREATE UNIQUE INDEX IF NOT EXISTS chat_room_group ON chat_room(conversation) WHERE revoked IS NULL AND kind='group';
|
|
58
|
-
CREATE TABLE IF NOT EXISTS chat_meta (
|
|
59
|
-
installation TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, updated TEXT NOT NULL,
|
|
60
|
-
PRIMARY KEY(installation, key)
|
|
61
|
-
);
|
|
62
|
-
`;
|
|
63
|
-
/** v88: flow decisions in the chat app, as on Telegram (telegram-flow.ts).
|
|
64
|
-
* A button is one opaque token for one visit (card, entry) of one card, on
|
|
65
|
-
* the part it rides; Edit and Send back open a prompt the person's next
|
|
66
|
-
* message in their DM answers. */
|
|
67
|
-
const CHAT_FLOW_SCHEMA = `
|
|
68
|
-
CREATE TABLE IF NOT EXISTS chat_flow_action (
|
|
69
|
-
token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
|
|
70
|
-
card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
|
|
71
|
-
action TEXT NOT NULL CHECK(action IN ('approve','edit','send-back')), expires TEXT NOT NULL, consumed TEXT
|
|
72
|
-
);
|
|
73
|
-
CREATE INDEX IF NOT EXISTS chat_flow_action_visit ON chat_flow_action(card, entry);
|
|
74
|
-
CREATE TABLE IF NOT EXISTS chat_flow_prompt (
|
|
75
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, binding INTEGER NOT NULL REFERENCES chat_binding(id),
|
|
76
|
-
card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
|
|
77
|
-
mode TEXT NOT NULL CHECK(mode IN ('edit','send-back')), created TEXT NOT NULL, expires TEXT NOT NULL, consumed TEXT
|
|
78
|
-
);
|
|
79
|
-
CREATE INDEX IF NOT EXISTS chat_flow_prompt_open ON chat_flow_prompt(binding, consumed);
|
|
80
|
-
`;
|
|
81
|
-
/** A flow's "Person chooses" zone in the chat app (flow-send.ts): one button per option (choice is its index, label its words
|
|
82
|
-
* when sent) for one visit of one card, on the part it rides. A reply in that notice's thread is the note; chat_flow_note
|
|
83
|
-
* is "Use this as your note?" Yes / No about one other message (held: its event; words: what it said, kept only until
|
|
84
|
-
* answered, as an event's own payload is cleared once it is handled), asked once per visit.
|
|
85
|
-
* Never in CHAT_TABLES: an older database has no such table until this schema creates it. */
|
|
86
|
-
const CHAT_FLOW_CHOICE_SCHEMA = `
|
|
87
|
-
CREATE TABLE IF NOT EXISTS chat_flow_choice (
|
|
88
|
-
token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
|
|
89
|
-
card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
|
|
90
|
-
choice INTEGER NOT NULL, label TEXT NOT NULL, expires TEXT NOT NULL, consumed TEXT
|
|
91
|
-
);
|
|
92
|
-
CREATE INDEX IF NOT EXISTS chat_flow_choice_visit ON chat_flow_choice(card, entry);
|
|
93
|
-
CREATE TABLE IF NOT EXISTS chat_flow_note (
|
|
94
|
-
token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
|
|
95
|
-
card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
|
|
96
|
-
held TEXT NOT NULL, words TEXT, answer TEXT NOT NULL CHECK(answer IN ('yes','no')), expires TEXT NOT NULL, consumed TEXT
|
|
97
|
-
);
|
|
98
|
-
CREATE INDEX IF NOT EXISTS chat_flow_note_visit ON chat_flow_note(card, entry);
|
|
99
|
-
`;
|
|
100
|
-
/** v93: a teammate's question in the chat app. One button per option (choice)
|
|
101
|
-
* and one to answer in words (choice NULL); that one opens a prompt the
|
|
102
|
-
* person's next message in their DM answers. Never in CHAT_TABLES: an older
|
|
103
|
-
* database has no such tables until this schema creates them. */
|
|
104
|
-
const CHAT_QUESTION_SCHEMA = `
|
|
105
|
-
CREATE TABLE IF NOT EXISTS chat_question_action (
|
|
106
|
-
token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
|
|
107
|
-
question INTEGER NOT NULL REFERENCES teammate_question(id), choice TEXT, expires TEXT NOT NULL, consumed TEXT
|
|
108
|
-
);
|
|
109
|
-
CREATE INDEX IF NOT EXISTS chat_question_action_question ON chat_question_action(question);
|
|
110
|
-
CREATE TABLE IF NOT EXISTS chat_question_prompt (
|
|
111
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, binding INTEGER NOT NULL REFERENCES chat_binding(id),
|
|
112
|
-
question INTEGER NOT NULL REFERENCES teammate_question(id), created TEXT NOT NULL, expires TEXT NOT NULL, consumed TEXT
|
|
113
|
-
);
|
|
114
|
-
CREATE INDEX IF NOT EXISTS chat_question_prompt_open ON chat_question_prompt(binding, consumed);
|
|
115
|
-
`;
|
|
116
|
-
/** The lead's question to its owner (ask_owner) in the chat app: one button per option (choice is its index) and one for
|
|
117
|
-
* "Something else" (choice NULL). A tap sends the option as the owner's next message. Never in CHAT_TABLES. */
|
|
118
|
-
const CHAT_ASK_SCHEMA = `
|
|
119
|
-
CREATE TABLE IF NOT EXISTS chat_ask_action (
|
|
120
|
-
token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id) ON DELETE CASCADE,
|
|
121
|
-
turn INTEGER NOT NULL, choice INTEGER, expires TEXT NOT NULL, consumed TEXT
|
|
122
|
-
);
|
|
123
|
-
CREATE INDEX IF NOT EXISTS chat_ask_action_turn ON chat_ask_action(turn);
|
|
124
|
-
`;
|
|
125
|
-
const CHAT_TABLES = [
|
|
126
|
-
"chat_binding",
|
|
127
|
-
"chat_pair",
|
|
128
|
-
"chat_event",
|
|
129
|
-
"chat_part",
|
|
130
|
-
"chat_action",
|
|
131
|
-
"chat_progress",
|
|
132
|
-
"chat_runtime",
|
|
133
|
-
];
|
|
4
|
+
import { chatSchema, chatTables } from "./contracts/chat-tables.js";
|
|
5
|
+
import { readChatPart, savedChatEventBody, savedChatPart } from "./contracts/chat-content.js";
|
|
6
|
+
export { savedChatPart };
|
|
7
|
+
export { chatSchema, chatTables };
|
|
134
8
|
export const chatHash = (text) => createHash("sha256").update(text).digest("hex");
|
|
135
9
|
export class ChatState {
|
|
136
10
|
store;
|
|
@@ -275,8 +149,9 @@ export class ChatState {
|
|
|
275
149
|
return this.bindingFor(identity.installation, member);
|
|
276
150
|
});
|
|
277
151
|
}
|
|
152
|
+
/** Save a received event with its body (contracts/chat-content.ts), versioned. */
|
|
278
153
|
enqueue(event) {
|
|
279
|
-
return (Number(this.prepare("INSERT OR IGNORE INTO chat_event(id,installation,binding,kind,channel,member,ts,thread,payload,created) VALUES(?,?,?,?,?,?,?,?,?,?)").run(event.id, event.installation, event.binding, event.kind, event.channel, event.member, event.ts, event.thread, event.payload, event.created).changes) === 1);
|
|
154
|
+
return (Number(this.prepare("INSERT OR IGNORE INTO chat_event(id,installation,binding,kind,channel,member,ts,thread,payload,created) VALUES(?,?,?,?,?,?,?,?,?,?)").run(event.id, event.installation, event.binding, event.kind, event.channel, event.member, event.ts, event.thread, savedChatEventBody(event.payload), event.created).changes) === 1);
|
|
280
155
|
}
|
|
281
156
|
event(id) {
|
|
282
157
|
return (this.prepare("SELECT * FROM chat_event WHERE id=?").get(id) ?? null);
|
|
@@ -296,7 +171,7 @@ export class ChatState {
|
|
|
296
171
|
plan(event, parts, now) {
|
|
297
172
|
this.store.transact(() => {
|
|
298
173
|
for (const [ordinal, part] of parts.entries()) {
|
|
299
|
-
const inserted = this.prepare("INSERT OR IGNORE INTO chat_part(event,ordinal,payload,created) VALUES(?,?,?,?)").run(event, ordinal,
|
|
174
|
+
const inserted = this.prepare("INSERT OR IGNORE INTO chat_part(event,ordinal,payload,created) VALUES(?,?,?,?)").run(event, ordinal, savedChatPart(part), now.toISOString());
|
|
300
175
|
if (Number(inserted.changes) && part.proposal)
|
|
301
176
|
this.tokens(Number(inserted.lastInsertRowid), part.proposal, ["confirm", "dismiss"], now);
|
|
302
177
|
if (Number(inserted.changes) && part.question)
|
|
@@ -332,16 +207,23 @@ export class ChatState {
|
|
|
332
207
|
return !!this.prepare("SELECT 1 FROM chat_runtime WHERE installation=? AND owner=? AND lease_until>?").get(installation, owner, now.toISOString());
|
|
333
208
|
}
|
|
334
209
|
}
|
|
335
|
-
|
|
336
|
-
export
|
|
210
|
+
/** A saved part's content. One that can't be read is a delivery problem that names the field, never a guess. */
|
|
211
|
+
export function partContent(payload) {
|
|
212
|
+
const read = readChatPart(payload);
|
|
213
|
+
if (!read.ok)
|
|
214
|
+
throw new ChatDeliveryError(`This saved message can't be read: ${read.issues.map(issue => issue.line).join("; ")}`, 0, false, true);
|
|
215
|
+
return read.value;
|
|
216
|
+
}
|
|
337
217
|
export class ChatDeliveryError extends Error {
|
|
338
218
|
code;
|
|
339
219
|
retryMs;
|
|
340
220
|
uncertain;
|
|
341
|
-
|
|
221
|
+
permanent;
|
|
222
|
+
constructor(code, retryMs = 5000, uncertain = false, permanent = false) {
|
|
342
223
|
super(code);
|
|
343
224
|
this.code = code;
|
|
344
225
|
this.retryMs = retryMs;
|
|
345
226
|
this.uncertain = uncertain;
|
|
227
|
+
this.permanent = permanent;
|
|
346
228
|
}
|
|
347
229
|
}
|
package/dist/chat-delivery.js
CHANGED
|
@@ -7,7 +7,9 @@ import { shapeReplyParts } from "./reply-shape.js";
|
|
|
7
7
|
import { warmTurn } from "./chat-warmth.js";
|
|
8
8
|
import { confirmMateProposal, dismissMateProposal, } from "./mate-doors.js";
|
|
9
9
|
import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
|
|
10
|
-
import { ChatState, ChatDeliveryError, chatHash, } from "./chat-delivery-state.js";
|
|
10
|
+
import { ChatState, ChatDeliveryError, chatHash, savedChatPart, } from "./chat-delivery-state.js";
|
|
11
|
+
import { readChatActionBody, readChatMessageBody, readChatPairBody, readChatPart } from "./contracts/chat-content.js";
|
|
12
|
+
import { readProposalActionRow } from "./contracts/chat-callback-rows.js";
|
|
11
13
|
import { answerChatFlowPrompt, answerChoiceMessage, applyChatFlowTap, flowDecisionParts, flowSendParts } from "./chat-flow.js";
|
|
12
14
|
import { connectChannel, FLOW_WORDS, takeChannelMessage, watchedChannel } from "./chat-inbox.js";
|
|
13
15
|
import { triggerConfigOf } from "./flow-triggers.js";
|
|
@@ -84,7 +86,10 @@ export async function processChatEvent(options) {
|
|
|
84
86
|
if (!state.owns(identity.installation, options.owner, nowOf(options)) ||
|
|
85
87
|
!options.current())
|
|
86
88
|
return false;
|
|
87
|
-
const
|
|
89
|
+
const body = readChatPairBody(event.payload);
|
|
90
|
+
const paired = body.ok
|
|
91
|
+
? state.pair(identity, body.value.hash, event.member, event.channel, nowOf(options))
|
|
92
|
+
: null;
|
|
88
93
|
if (!paired) {
|
|
89
94
|
state.finish(event.id, true);
|
|
90
95
|
return true;
|
|
@@ -112,7 +117,12 @@ export async function processChatEvent(options) {
|
|
|
112
117
|
applyChatAction(options, event, binding, repos);
|
|
113
118
|
return true;
|
|
114
119
|
}
|
|
115
|
-
const
|
|
120
|
+
const body = readChatMessageBody(event.payload);
|
|
121
|
+
if (!body.ok) {
|
|
122
|
+
state.plan(event.id, [{ text: `That message couldn't be read (${body.issues.map(issue => issue.line).join("; ")}). Send it again.` }], nowOf(options));
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const input = body.value;
|
|
116
126
|
if (typeof input.unsupported === "string") {
|
|
117
127
|
state.plan(event.id, [{ text: input.unsupported }], nowOf(options));
|
|
118
128
|
return true;
|
|
@@ -193,8 +203,7 @@ export async function processChatEvent(options) {
|
|
|
193
203
|
if (text.length > MATE_MESSAGE_MAX_CHARS) {
|
|
194
204
|
state.plan(event.id, [
|
|
195
205
|
{
|
|
196
|
-
text: tooLongText(
|
|
197
|
-
text.length),
|
|
206
|
+
text: tooLongText(input.originalLength || text.length),
|
|
198
207
|
},
|
|
199
208
|
], nowOf(options));
|
|
200
209
|
return true;
|
|
@@ -237,7 +246,8 @@ export async function processChatEvent(options) {
|
|
|
237
246
|
const contexts = state
|
|
238
247
|
.prepare("SELECT p.payload FROM chat_part p JOIN chat_event e ON e.id=p.event WHERE e.binding=? AND e.channel=? AND (p.message=? OR e.thread=?) AND p.state='sent' ORDER BY p.id DESC LIMIT 100")
|
|
239
248
|
.all(binding.id, binding.channel, event.thread, event.thread)
|
|
240
|
-
|
|
249
|
+
// Context only: a part that can't be read adds none.
|
|
250
|
+
.flatMap((row) => { const read = readChatPart(String(row.payload)); return read.ok ? [read.value] : []; })
|
|
241
251
|
// A card is about the task it names, or the one confirming it filed.
|
|
242
252
|
.map((c) => {
|
|
243
253
|
if (c.task || c.proposal === undefined)
|
|
@@ -385,6 +395,11 @@ export async function processChatEvent(options) {
|
|
|
385
395
|
const problem = error instanceof ChatDeliveryError
|
|
386
396
|
? error
|
|
387
397
|
: new ChatDeliveryError(`${options.label} is waiting to retry`);
|
|
398
|
+
if (problem.permanent) {
|
|
399
|
+
state.plan(event.id, [{ text: problem.message }], nowOf(options));
|
|
400
|
+
state.prepare("UPDATE chat_event SET next_at=NULL,problem=? WHERE id=?").run(problem.message, event.id);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
388
403
|
state
|
|
389
404
|
.prepare("UPDATE chat_runtime SET problem=? WHERE installation=? AND owner=?")
|
|
390
405
|
.run(problem.message, identity.installation, options.owner);
|
|
@@ -405,8 +420,11 @@ async function channelInboxEvent(options, event) {
|
|
|
405
420
|
const binding = event.binding === null ? null : state.bindingById(event.binding);
|
|
406
421
|
if (binding !== null && event.channel === binding.channel)
|
|
407
422
|
return false;
|
|
408
|
-
const
|
|
409
|
-
|
|
423
|
+
const body = readChatMessageBody(event.payload);
|
|
424
|
+
// One that can't be read is answered, by path, on the ordinary path.
|
|
425
|
+
if (!body.ok)
|
|
426
|
+
return false;
|
|
427
|
+
const text = body.value.text ?? "";
|
|
410
428
|
if (FLOW_WORDS.test(text.trim())) {
|
|
411
429
|
// Only a paired approver connects a channel, and only to a flow in their projects.
|
|
412
430
|
if (binding === null || !state.live(binding)) {
|
|
@@ -450,7 +468,14 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
450
468
|
event.member !== binding.member ||
|
|
451
469
|
(event.channel !== binding.channel && state.room(options.identity.installation, event.channel)?.kind !== "group"))
|
|
452
470
|
return;
|
|
453
|
-
|
|
471
|
+
// A button the paired person tapped whose data couldn't be read is answered plainly: what was wrong, and that nothing was done.
|
|
472
|
+
const body = readChatActionBody(event.payload);
|
|
473
|
+
if (!body.ok || "problem" in body.value) {
|
|
474
|
+
const problem = body.ok ? ("problem" in body.value ? body.value.problem : "") : body.issues.map(issue => issue.line).join("; ");
|
|
475
|
+
state.plan(event.id, [{ text: `That button couldn't be read (${problem}). Nothing was done.` }], now);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const token = body.value.token;
|
|
454
479
|
// A flow decision's button (v88) is answered by the flow's own door.
|
|
455
480
|
if (applyChatFlowTap({ store, state, label: options.label }, event, binding, token, repos, now))
|
|
456
481
|
return;
|
|
@@ -460,16 +485,19 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
460
485
|
// The lead's question to its owner: the tapped option becomes their next message.
|
|
461
486
|
if (applyChatAskTap({ store, state }, event, binding, token, now))
|
|
462
487
|
return;
|
|
463
|
-
const
|
|
488
|
+
const saved = state
|
|
464
489
|
.prepare("SELECT a.*,p.message,e.binding,e.channel,e.thread FROM chat_action a JOIN chat_part p ON p.id=a.part JOIN chat_event e ON e.id=p.event WHERE token=?")
|
|
465
490
|
.get(token);
|
|
491
|
+
// The saved button, read by its schema: one that can't be read is a spent button, never a guess.
|
|
492
|
+
const read = saved === undefined ? null : readProposalActionRow({ ...saved });
|
|
493
|
+
const action = read?.ok === true ? read.value : null;
|
|
466
494
|
const invalid = !action ||
|
|
467
495
|
action.binding !== binding.id ||
|
|
468
496
|
action.channel !== event.channel ||
|
|
469
497
|
action.message !== event.ts ||
|
|
470
498
|
(options.state.channel === "slack" && action.thread !== event.thread) ||
|
|
471
499
|
action.consumed !== null ||
|
|
472
|
-
|
|
500
|
+
action.expires <= now.toISOString();
|
|
473
501
|
if (invalid) {
|
|
474
502
|
state.plan(event.id, [
|
|
475
503
|
{
|
|
@@ -478,7 +506,7 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
478
506
|
], now);
|
|
479
507
|
return;
|
|
480
508
|
}
|
|
481
|
-
const proposal = store.getMateProposal(
|
|
509
|
+
const proposal = store.getMateProposal(action.proposal);
|
|
482
510
|
const verified = verifyApproverStanding(store, binding.approver, binding.generation, repos);
|
|
483
511
|
if (!proposal || !verified.ok) {
|
|
484
512
|
state.finish(event.id, true);
|
|
@@ -490,14 +518,14 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
490
518
|
edit: event.ts,
|
|
491
519
|
};
|
|
492
520
|
const preview = proposalPreview(store, proposal, repos, options.state.channel);
|
|
493
|
-
const phase =
|
|
521
|
+
const phase = action.phase;
|
|
494
522
|
if (proposal.state !== "pending")
|
|
495
523
|
content = { text: proposalOutcomeText(proposal), edit: event.ts };
|
|
496
524
|
else if (!preview.buttons ||
|
|
497
525
|
preview.text.length > (options.maxProposal ?? 10_000))
|
|
498
526
|
content = { ...content, text: "Review this action in Toolroll." };
|
|
499
527
|
else if (phase === "cancel") {
|
|
500
|
-
state.tokens(
|
|
528
|
+
state.tokens(action.part, proposal.id, ["confirm", "dismiss"], now);
|
|
501
529
|
}
|
|
502
530
|
else if (phase === "dismiss") {
|
|
503
531
|
const dismissed = dismissMateProposal(store, verified.who, proposal.id, now);
|
|
@@ -512,7 +540,7 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
512
540
|
proposal.kind === "answer" &&
|
|
513
541
|
proposal.payload.reversible === false) {
|
|
514
542
|
content = { ...content, phase: "armed" };
|
|
515
|
-
state.tokens(
|
|
543
|
+
state.tokens(action.part, proposal.id, ["yes", "cancel"], now);
|
|
516
544
|
}
|
|
517
545
|
else {
|
|
518
546
|
const outcome = confirmMateProposal(store, verified.who, proposal.id, now, {
|
|
@@ -524,7 +552,7 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
524
552
|
});
|
|
525
553
|
if (!outcome.ok && outcome.reason === "needs-confirm") {
|
|
526
554
|
content = { ...content, phase: "armed" };
|
|
527
|
-
state.tokens(
|
|
555
|
+
state.tokens(action.part, proposal.id, ["yes", "cancel"], now);
|
|
528
556
|
}
|
|
529
557
|
else {
|
|
530
558
|
content = {
|
|
@@ -540,7 +568,7 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
540
568
|
// Repaint the original persisted card; retain one placement and fresh tokens.
|
|
541
569
|
state
|
|
542
570
|
.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?")
|
|
543
|
-
.run(
|
|
571
|
+
.run(savedChatPart(content), action.part);
|
|
544
572
|
if (!content.proposal)
|
|
545
573
|
state
|
|
546
574
|
.prepare("UPDATE chat_action SET consumed=? WHERE proposal=? AND consumed IS NULL")
|
|
@@ -607,7 +635,7 @@ export async function planRoomMessages(options) {
|
|
|
607
635
|
const id = chatHash(`${state.channel}:room:${room.id}:${message.id}`);
|
|
608
636
|
const target = carrier;
|
|
609
637
|
store.transact(() => {
|
|
610
|
-
if (parts.length > 0 && state.enqueue({ id, installation: identity.installation, binding: target.id, kind: "message", channel: room.chat, member: target.member, ts: "", thread: "", payload:
|
|
638
|
+
if (parts.length > 0 && state.enqueue({ id, installation: identity.installation, binding: target.id, kind: "message", channel: room.chat, member: target.member, ts: "", thread: "", payload: {}, created: now.toISOString() }))
|
|
611
639
|
state.plan(id, parts, now);
|
|
612
640
|
state.advanceRoomCursor(room.id, message.id);
|
|
613
641
|
});
|
|
@@ -629,13 +657,13 @@ function planQuietCard(options, binding, notification, now) {
|
|
|
629
657
|
if (card.message !== null) {
|
|
630
658
|
if (card.digest === shown)
|
|
631
659
|
return;
|
|
632
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(
|
|
660
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(savedChatPart(content), now.toISOString(), Number(card.message));
|
|
633
661
|
store.setChatCardMessage(card.id, card.message, shown);
|
|
634
662
|
return;
|
|
635
663
|
}
|
|
636
664
|
const id = chatHash(`${state.channel}:card:${binding.id}:${card.id}`);
|
|
637
665
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
638
|
-
ts: "", thread: "", payload:
|
|
666
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
639
667
|
state.plan(id, [content], now);
|
|
640
668
|
const part = state.prepare("SELECT id FROM chat_part WHERE event=?").get(id);
|
|
641
669
|
if (part !== undefined)
|
|
@@ -659,13 +687,13 @@ function planFinished(options, binding, taskRef, run, now, notification = null)
|
|
|
659
687
|
if (batch.message !== null) {
|
|
660
688
|
if (batch.digest === shown)
|
|
661
689
|
return true;
|
|
662
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(
|
|
690
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(savedChatPart(content), now.toISOString(), Number(batch.message));
|
|
663
691
|
store.setChatBatchMessage(batch.id, batch.message, shown);
|
|
664
692
|
return true;
|
|
665
693
|
}
|
|
666
694
|
const id = chatHash(`${state.channel}:batch:${binding.id}:${batch.id}`);
|
|
667
695
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
668
|
-
ts: "", thread: "", payload:
|
|
696
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
669
697
|
state.plan(id, [content], now);
|
|
670
698
|
const part = state.prepare("SELECT id FROM chat_part WHERE event=?").get(id);
|
|
671
699
|
if (part !== undefined)
|
|
@@ -714,7 +742,7 @@ function planResultShots(options, binding, notification, repos, id, now) {
|
|
|
714
742
|
image: image(plan.shots[0].artifact, plan.shots[0].sha256), shot: { follows: null }, ...where }]
|
|
715
743
|
: plan.shots.map(one => ({ text: one.caption, image: image(one.artifact, one.sha256), shot: { follows }, ...where }));
|
|
716
744
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
717
|
-
ts: "", thread: "", payload:
|
|
745
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
718
746
|
state.plan(id, parts, now);
|
|
719
747
|
}
|
|
720
748
|
/** One progress card per exact result; separate urgent facts retain their own review link. */
|
|
@@ -770,11 +798,11 @@ export async function planChatNotifications(options) {
|
|
|
770
798
|
const earlier = leadSayEarlier(store, notification).map(one => state.prepare("SELECT id FROM chat_part WHERE event=?")
|
|
771
799
|
.get(chatHash(`${options.state.channel}:notice:${binding.id}:${one}`))).find(one => one !== undefined);
|
|
772
800
|
if (earlier !== undefined) {
|
|
773
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(
|
|
801
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(savedChatPart(content), now.toISOString(), Number(earlier.id));
|
|
774
802
|
return;
|
|
775
803
|
}
|
|
776
804
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
777
|
-
ts: "", thread: "", payload:
|
|
805
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
778
806
|
state.plan(id, [content], now);
|
|
779
807
|
return;
|
|
780
808
|
}
|
|
@@ -826,7 +854,7 @@ export async function planChatNotifications(options) {
|
|
|
826
854
|
member: binding.member,
|
|
827
855
|
ts: "",
|
|
828
856
|
thread: "",
|
|
829
|
-
payload:
|
|
857
|
+
payload: {},
|
|
830
858
|
created: now.toISOString(),
|
|
831
859
|
});
|
|
832
860
|
state.plan(id, [content], now);
|
|
@@ -843,20 +871,20 @@ export async function planChatNotifications(options) {
|
|
|
843
871
|
const parts = flowDecisionParts(store, notification, state.channel);
|
|
844
872
|
if (parts !== null) {
|
|
845
873
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
846
|
-
ts: "", thread: "", payload:
|
|
874
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
847
875
|
state.plan(id, parts, now);
|
|
848
876
|
}
|
|
849
877
|
}
|
|
850
878
|
else if (personal && flowSendParts(store, notification, state.channel) !== null) {
|
|
851
879
|
// A flow's "Send to me" (flow-send.ts): what was done, with its links as buttons.
|
|
852
880
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
853
|
-
ts: "", thread: "", payload:
|
|
881
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
854
882
|
state.plan(id, flowSendParts(store, notification, state.channel), now);
|
|
855
883
|
}
|
|
856
884
|
else if (notification.kind === "flow-card" && questionParts(store, notification, binding) !== null) {
|
|
857
885
|
// A teammate's question (v93): its options and "Answer in words" on the notice, for the person it asks.
|
|
858
886
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
859
|
-
ts: "", thread: "", payload:
|
|
887
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
860
888
|
state.plan(id, questionParts(store, notification, binding), now);
|
|
861
889
|
}
|
|
862
890
|
else if (notification.pushClass !== null || personal || quiet) {
|
|
@@ -869,7 +897,7 @@ export async function planChatNotifications(options) {
|
|
|
869
897
|
member: binding.member,
|
|
870
898
|
ts: "",
|
|
871
899
|
thread: "",
|
|
872
|
-
payload:
|
|
900
|
+
payload: {},
|
|
873
901
|
created: now.toISOString(),
|
|
874
902
|
});
|
|
875
903
|
state.plan(id, [
|
package/dist/chat-flow.js
CHANGED
|
@@ -34,7 +34,8 @@ import { renderReply } from "./reply-shape.js";
|
|
|
34
34
|
import { deciderOf, replyTarget } from "./flows.js";
|
|
35
35
|
import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
36
36
|
import { verifyApproverStanding } from "./principal.js";
|
|
37
|
-
import { chatHash } from "./chat-delivery-state.js";
|
|
37
|
+
import { chatHash, partContent, savedChatPart } from "./chat-delivery-state.js";
|
|
38
|
+
import { readHeldWords } from "./contracts/chat-content.js";
|
|
38
39
|
import { FLOW_DECIDE_KEY, flowDecisionAt } from "./telegram-flow.js";
|
|
39
40
|
import { phoneCommand } from "./telegram-status.js";
|
|
40
41
|
export const CHAT_FLOW_LABELS = { approve: "Approve", edit: "Edit", "send-back": "Send back" };
|
|
@@ -144,11 +145,11 @@ const retire = (state, card, entry, now) => {
|
|
|
144
145
|
/** Show the tapped notice again with what happened, and without its buttons. */
|
|
145
146
|
function repaint(state, part, event, line) {
|
|
146
147
|
const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
|
|
147
|
-
const before = row === undefined ? { text: "" } :
|
|
148
|
+
const before = row === undefined ? { text: "" } : partContent(String(row["payload"]));
|
|
148
149
|
// An item list (flow-items.ts) stays whole and keeps its bold titles and labelled links; only plain text is capped.
|
|
149
150
|
const content = { text: before.voice === true ? `${before.text}\n\n${line}` : `${before.text}\n\n${line}`.slice(0, PART_CHARS + 400), edit: event.ts, ...(before.voice === true ? { voice: true } : {}),
|
|
150
151
|
...(before.link === undefined ? {} : { link: before.link }), ...(before.channel === undefined ? {} : { channel: before.channel }) };
|
|
151
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(
|
|
152
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(savedChatPart(content), part);
|
|
152
153
|
}
|
|
153
154
|
/**
|
|
154
155
|
* A tapped flow button, inside the action's transaction. false when the
|
|
@@ -351,8 +352,9 @@ function applyChatNoteTap(options, event, binding, token, repos, now) {
|
|
|
351
352
|
}
|
|
352
353
|
state.prepare("UPDATE chat_flow_note SET consumed=?,words=NULL WHERE part=?").run(now.toISOString(), part);
|
|
353
354
|
const held = state.event(String(tapped["held"]));
|
|
354
|
-
const
|
|
355
|
-
|
|
355
|
+
const kept = typeof tapped["words"] === "string" ? readHeldWords(tapped["words"]) : null;
|
|
356
|
+
const words = kept?.ok === true ? kept.value : null;
|
|
357
|
+
if (held === null || words === null) {
|
|
356
358
|
repaint(state, part, event, "That message is no longer kept; nothing was changed.");
|
|
357
359
|
state.finish(event.id);
|
|
358
360
|
return true;
|
|
@@ -360,7 +362,7 @@ function applyChatNoteTap(options, event, binding, token, repos, now) {
|
|
|
360
362
|
if (tapped["answer"] === "no") {
|
|
361
363
|
// Passed on as the message it was, to the lead, once.
|
|
362
364
|
state.enqueue({ id: chatHash(`${held.id}:lead`), installation: held.installation, binding: held.binding, kind: "message", channel: held.channel, member: held.member,
|
|
363
|
-
ts: held.ts, thread: held.thread, payload:
|
|
365
|
+
ts: held.ts, thread: held.thread, payload: { ...words, lead: true }, created: now.toISOString() });
|
|
364
366
|
repaint(state, part, event, "Passed to the lead.");
|
|
365
367
|
state.finish(event.id);
|
|
366
368
|
return true;
|
package/dist/chat-inbox.js
CHANGED
|
@@ -116,7 +116,7 @@ export function replyInChannel(store, source, visit, text, now) {
|
|
|
116
116
|
const id = chatHash(`${source.app}:flow-reply:${visit.card}:${visit.entry}`);
|
|
117
117
|
store.transact(() => {
|
|
118
118
|
if (state.enqueue({ id, installation: grant.installation, binding: grant.id, kind: "message", channel: source.conversation, member: grant.member,
|
|
119
|
-
ts: source.thread, thread: source.thread, payload:
|
|
119
|
+
ts: source.thread, thread: source.thread, payload: {}, created: now.toISOString() }))
|
|
120
120
|
state.plan(id, [{ text: text.slice(0, 3500), channel: source.conversation }], now);
|
|
121
121
|
});
|
|
122
122
|
return { ok: true };
|