approval-md 0.4.3 → 0.4.6
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/SPEC.md +13 -2
- package/dist/src/channels/telegram.d.ts +105 -31
- package/dist/src/channels/telegram.js +157 -81
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +100 -35
- package/dist/src/cli/channel-telegram.js +216 -119
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/doctor.js +33 -13
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/help.js +3 -3
- package/dist/src/cli/hook-exec-code.d.ts +150 -0
- package/dist/src/cli/hook-exec-code.js +357 -0
- package/dist/src/cli/hook-exec-code.js.map +1 -0
- package/dist/src/cli/hook.d.ts +125 -1
- package/dist/src/cli/hook.js +2177 -107
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/core/command-class.d.ts +368 -1
- package/dist/src/core/command-class.js +2830 -93
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/date-argv.d.ts +83 -0
- package/dist/src/core/date-argv.js +212 -0
- package/dist/src/core/date-argv.js.map +1 -0
- package/dist/src/core/exec-code.d.ts +77 -0
- package/dist/src/core/exec-code.js +60 -0
- package/dist/src/core/exec-code.js.map +1 -0
- package/dist/src/core/git-push-argv.d.ts +111 -0
- package/dist/src/core/git-push-argv.js +375 -0
- package/dist/src/core/git-push-argv.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +71 -30
- package/dist/src/core/harness-version.js +101 -34
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +8 -0
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/read-scope.js +1 -0
- package/dist/src/core/read-scope.js.map +1 -1
- package/dist/src/core/sysinfo-argv.d.ts +48 -0
- package/dist/src/core/sysinfo-argv.js +340 -0
- package/dist/src/core/sysinfo-argv.js.map +1 -0
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/docs/README-extended.md +3 -2
- package/docs/cli-reference.md +20 -7
- package/package.json +2 -2
- package/schema/event.schema.json +1 -1
- package/schema/fixtures/policy/invalid/exec-code-custody-boolean.json +11 -0
- package/schema/fixtures/policy/invalid/exec-code-execute-code-value.json +11 -0
- package/schema/fixtures/policy/invalid/exec-code-unknown-key.json +12 -0
- package/schema/fixtures/policy/valid/exec-code-both.json +17 -0
- package/schema/fixtures/policy/valid/exec-code-custody.json +11 -0
- package/schema/policy.schema.json +23 -0
|
@@ -310,27 +310,84 @@ export const TELEGRAM_STALE_UNKNOWN = "This request is not open here — it was
|
|
|
310
310
|
*/
|
|
311
311
|
export const TELEGRAM_PROMPT_HEADING = "APPROVAL REQUIRED";
|
|
312
312
|
/**
|
|
313
|
-
* What the label over the payload chunks names (APRV-162)
|
|
313
|
+
* What the label over the payload chunks names (APRV-162), in a resident's
|
|
314
|
+
* words (P-9).
|
|
314
315
|
*
|
|
315
316
|
* The chunks carry the canonical rendering, which is a deterministic function
|
|
316
|
-
* of the bytes and not the bytes themselves
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
317
|
+
* of the bytes and not the bytes themselves, and the rendering names its own
|
|
318
|
+
* `display_hash` and store path inside the block (SPEC.md §10.3, bound
|
|
319
|
+
* verbatim by `display_hash`). This label sits OUTSIDE that block, so it is
|
|
320
|
+
* free to say what the block is for the person reading it: what they are
|
|
321
|
+
* approving. Before P-9 it said "the canonical rendering this approval's
|
|
322
|
+
* display_hash names; raw bytes at the store path inside", which is the
|
|
323
|
+
* block's own first lines restated in the system's vocabulary.
|
|
320
324
|
*/
|
|
321
|
-
export const PAYLOAD_CHUNK_LABEL_TAIL = "
|
|
322
|
-
export const PAYLOAD_CHUNK_LABEL = `
|
|
325
|
+
export const PAYLOAD_CHUNK_LABEL_TAIL = "what you are approving";
|
|
326
|
+
export const PAYLOAD_CHUNK_LABEL = `Exactly ${PAYLOAD_CHUNK_LABEL_TAIL}`;
|
|
327
|
+
/**
|
|
328
|
+
* The plain-words label over lines the requesting agent wrote (P-9).
|
|
329
|
+
*
|
|
330
|
+
* The same words the minimal card uses for the agent's own summary: the reader
|
|
331
|
+
* learns that the agent said it and that nobody checked it, which is what the
|
|
332
|
+
* old "CLAIMED ... NOT verified by the runtime" said in the system's
|
|
333
|
+
* vocabulary. The author id stays after it, because which agent said a thing
|
|
334
|
+
* is part of what an approver weighs (APRV-144).
|
|
335
|
+
*/
|
|
336
|
+
export const TELEGRAM_CLAIMED_LABEL = "What your agent says (not checked)";
|
|
323
337
|
/**
|
|
324
338
|
* What the claimed block is headed, and what a second claimed message is headed
|
|
325
339
|
* when a rationale overflows one (APRV-165).
|
|
326
340
|
*
|
|
327
|
-
* Both say
|
|
328
|
-
* message a reader may see first, and a claimed line that arrives under
|
|
329
|
-
* heading at all reads as the runtime's own.
|
|
341
|
+
* Both say the lines are the agent's and not checked, because a continuation
|
|
342
|
+
* is a message a reader may see first, and a claimed line that arrives under
|
|
343
|
+
* no heading at all reads as the runtime's own.
|
|
344
|
+
*/
|
|
345
|
+
export const TELEGRAM_CLAIMED_HEADING_PREFIX = `${TELEGRAM_CLAIMED_LABEL} —`;
|
|
346
|
+
export const TELEGRAM_CLAIMED_CONTINUED_HEADING = "What your agent says (continued, not checked)";
|
|
347
|
+
/**
|
|
348
|
+
* The heading over the rows the runtime derived itself (P-9): the class, the
|
|
349
|
+
* wait, the budget. Before P-9 it read "COMPUTED — derived by the runtime from
|
|
350
|
+
* the log, the policy and the payload bytes". The rows keep their origin tags,
|
|
351
|
+
* which the village relay reads (its quiet hold matches the `ttl` row up to
|
|
352
|
+
* its `<i>(` tag).
|
|
353
|
+
*/
|
|
354
|
+
export const TELEGRAM_COMPUTED_HEADING = "Checked by approval.md";
|
|
355
|
+
/** The most characters of an author id the claimed label shows (fold round 1, S3). */
|
|
356
|
+
export const TELEGRAM_CLAIMED_AUTHOR_MAX = 80;
|
|
357
|
+
/**
|
|
358
|
+
* {@link TELEGRAM_CLAIMED_LABEL} naming who wrote the lines under it.
|
|
359
|
+
*
|
|
360
|
+
* The author is the requester's actor id, which the actor rule
|
|
361
|
+
* (`^(human|agent):.+`) checks only at its start: a line break later in it
|
|
362
|
+
* passes. Escaping stops markup, but not a second line inside the bold heading
|
|
363
|
+
* that reads like the runtime's own "Checked by approval.md" (fold round 1,
|
|
364
|
+
* S3). So every run of whitespace, line breaks included, becomes one space,
|
|
365
|
+
* control and format characters are dropped, and the id is cut at
|
|
366
|
+
* {@link TELEGRAM_CLAIMED_AUTHOR_MAX} characters with an ellipsis. The label is
|
|
367
|
+
* always one line.
|
|
368
|
+
*/
|
|
369
|
+
export function claimedLabelFor(author) {
|
|
370
|
+
return `${TELEGRAM_CLAIMED_LABEL} — ${oneLineAuthor(author)}`;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* An author id as one short line (fold round 1, S3): whitespace runs, line
|
|
374
|
+
* breaks included, become one space; control and format characters go; the id
|
|
375
|
+
* is cut at {@link TELEGRAM_CLAIMED_AUTHOR_MAX} characters with an ellipsis.
|
|
376
|
+
* Used by {@link claimedLabelFor} and by the `(author)` tag on each claimed
|
|
377
|
+
* row, which carries the same id. A computed row's origin is a fixed word, so
|
|
378
|
+
* this changes nothing there.
|
|
330
379
|
*/
|
|
331
|
-
export
|
|
332
|
-
|
|
333
|
-
|
|
380
|
+
export function oneLineAuthor(author) {
|
|
381
|
+
const flat = author
|
|
382
|
+
.replace(/\s+/gu, " ")
|
|
383
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, "")
|
|
384
|
+
.trim();
|
|
385
|
+
const chars = Array.from(flat);
|
|
386
|
+
const shown = chars.length > TELEGRAM_CLAIMED_AUTHOR_MAX
|
|
387
|
+
? `${chars.slice(0, TELEGRAM_CLAIMED_AUTHOR_MAX - 1).join("")}…`
|
|
388
|
+
: flat;
|
|
389
|
+
return shown.length === 0 ? "unknown" : shown;
|
|
390
|
+
}
|
|
334
391
|
/**
|
|
335
392
|
* The most members one digest may carry (APRV-115).
|
|
336
393
|
*
|
|
@@ -736,16 +793,16 @@ export function renderTelegram(request, heading = TELEGRAM_PROMPT_HEADING, layou
|
|
|
736
793
|
claimedLines.push(entry);
|
|
737
794
|
}
|
|
738
795
|
const author = request.summary.kind === "claimed" ? request.summary.author : "the requesting party";
|
|
739
|
-
const render = (entry) => `• <b>${escapeHtml(entry.label)}:</b> ${escapeHtml(entry.text)} <i>(${escapeHtml(entry.origin)})</i>`;
|
|
796
|
+
const render = (entry) => `• <b>${escapeHtml(entry.label)}:</b> ${escapeHtml(entry.text)} <i>(${escapeHtml(oneLineAuthor(entry.origin))})</i>`;
|
|
740
797
|
const header = [
|
|
741
798
|
`<b>${escapeHtml(heading)}</b>`,
|
|
742
799
|
`<code>${escapeHtml(request.action_key.value)}</code>`,
|
|
743
800
|
"",
|
|
744
|
-
|
|
801
|
+
`<b>${TELEGRAM_COMPUTED_HEADING}</b>`,
|
|
745
802
|
...computedLines.map(render),
|
|
746
803
|
].join("\n");
|
|
747
804
|
const claimedText = [
|
|
748
|
-
`<b>${
|
|
805
|
+
`<b>${escapeHtml(claimedLabelFor(author))}</b>`,
|
|
749
806
|
...claimedLines.map(render),
|
|
750
807
|
].join("\n");
|
|
751
808
|
return {
|
|
@@ -1048,44 +1105,33 @@ export function groupForDigest(requests, max = TELEGRAM_DIGEST_MAX_MEMBERS) {
|
|
|
1048
1105
|
return groups;
|
|
1049
1106
|
}
|
|
1050
1107
|
/**
|
|
1051
|
-
* The computed
|
|
1108
|
+
* The one computed line a digest states about its members (P-9).
|
|
1052
1109
|
*
|
|
1053
|
-
*
|
|
1054
|
-
* grouping key made them
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1110
|
+
* Read off the members themselves, which is sound precisely because the
|
|
1111
|
+
* grouping key made them alike: a digest whose members disagreed about their
|
|
1112
|
+
* class or their task is a digest the listener would not have built. What the
|
|
1113
|
+
* line says is the thing an approver needs before the buttons: why these are
|
|
1114
|
+
* one message, and that every request it decides is shown in full above it
|
|
1115
|
+
* (SPEC.md §10.3).
|
|
1116
|
+
*
|
|
1117
|
+
* Before P-9 this was five facts with origin tags: the class name, the
|
|
1118
|
+
* autonomy, the task id, the grouping key (`keys:body,from,subject,to`) and the
|
|
1119
|
+
* payload binding. Each is still on every member's own card above, where an
|
|
1120
|
+
* operator who wants it finds it; the digest is where a resident answers, and
|
|
1121
|
+
* there it was ids and vocabulary between them and the buttons.
|
|
1058
1122
|
*/
|
|
1059
|
-
export function
|
|
1123
|
+
export function digestLine(members) {
|
|
1060
1124
|
const first = members[0];
|
|
1061
1125
|
if (first === undefined)
|
|
1062
|
-
return
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
{ label: "class", text: classes.join(", "), origin: originOf(first.class) },
|
|
1072
|
-
{ label: "autonomy", text: autonomies.join(", "), origin: originOf(first.autonomy) },
|
|
1073
|
-
{ label: "task", text: first.task.value ?? "(none)", origin: originOf(first.task) },
|
|
1074
|
-
{
|
|
1075
|
-
label: "grouped by",
|
|
1076
|
-
text: shared === null
|
|
1077
|
-
? `one class, one task, one payload shape (${payloadShapeKey(first.fullPayload.value?.value)})`
|
|
1078
|
-
: `one tool call: one task, one payload, ${String(classes.length)} class(es) of the same command`,
|
|
1079
|
-
origin: "grouping",
|
|
1080
|
-
},
|
|
1081
|
-
{
|
|
1082
|
-
label: "payloads",
|
|
1083
|
-
text: shared === null
|
|
1084
|
-
? `${members.length} full payloads, one per request, in the ${members.length} prompts above this message`
|
|
1085
|
-
: `one payload, shared by all ${members.length} requests, in the prompt above this message`,
|
|
1086
|
-
origin: originOf(first.fullPayload),
|
|
1087
|
-
},
|
|
1088
|
-
];
|
|
1126
|
+
return "";
|
|
1127
|
+
const count = String(members.length);
|
|
1128
|
+
if (sharedPayload(members) !== null) {
|
|
1129
|
+
return `All ${count} are about one action, shown once in full above.`;
|
|
1130
|
+
}
|
|
1131
|
+
const task = first.task.value;
|
|
1132
|
+
return task === null || task.length === 0
|
|
1133
|
+
? `All ${count} are the same kind of request. Each one is shown in full above.`
|
|
1134
|
+
: `All ${count} come from the same task. Each one is shown in full above.`;
|
|
1089
1135
|
}
|
|
1090
1136
|
/**
|
|
1091
1137
|
* The one payload every member is about, or `null` when they differ
|
|
@@ -1113,36 +1159,56 @@ function sharedPayload(members) {
|
|
|
1113
1159
|
}
|
|
1114
1160
|
/**
|
|
1115
1161
|
* The collapsed re-delivery's own message: what is waiting, and one way to
|
|
1116
|
-
* clear it (APRV-287).
|
|
1162
|
+
* clear it (APRV-287), in a resident's words (P-9).
|
|
1117
1163
|
*
|
|
1118
|
-
* Everything above the
|
|
1119
|
-
*
|
|
1120
|
-
* agent's own summaries under a
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1164
|
+
* Everything above the claimed label is computed by the runtime from the
|
|
1165
|
+
* verified log (the reminder's count, why nobody is waiting, each member's
|
|
1166
|
+
* age), and the per-member lines carry the agent's own summaries under a label
|
|
1167
|
+
* that says they are the agent's and were not checked, exactly as an ordinary
|
|
1168
|
+
* digest does (SPEC.md §9: computed and claimed stay visibly separate). What it
|
|
1169
|
+
* does NOT carry is any payload, and the footer says so in the same breath as
|
|
1170
|
+
* it explains why the only button rejects: a decision is bound to the bytes the
|
|
1171
|
+
* approver was shown, and nothing here shows them.
|
|
1172
|
+
*
|
|
1173
|
+
* Before P-9 each member line led with its action key, and the text explained
|
|
1174
|
+
* the hook's wait, log events and `approval grant <action key>`. None of that
|
|
1175
|
+
* helps the person holding the phone, so a member is now its summary and its
|
|
1176
|
+
* age, and the footer says what they can do: use the earlier message if its
|
|
1177
|
+
* buttons still work (`staleCopy`), or let the agent ask again.
|
|
1124
1178
|
*/
|
|
1125
1179
|
function renderStaleSummary(digest, stale, open, total) {
|
|
1180
|
+
// P-9: the restart's one reminder, while every member is still open. Its
|
|
1181
|
+
// first line is the headline; any further line (what follows this message)
|
|
1182
|
+
// goes after the computed lines, which say what this message lists.
|
|
1183
|
+
const reminder = open === total ? (stale.reminder ?? []) : [];
|
|
1184
|
+
const headline = reminder[0] ??
|
|
1185
|
+
(open === 0
|
|
1186
|
+
? `All ${String(total)} older requests decided`
|
|
1187
|
+
: open === total
|
|
1188
|
+
? `${String(open)} older request${open === 1 ? " is" : "s are"} still open`
|
|
1189
|
+
: `${String(open)} of ${String(total)} older requests still open`);
|
|
1126
1190
|
const lines = [
|
|
1127
|
-
`<b>${escapeHtml(
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
"",
|
|
1131
|
-
"<b>COMPUTED — derived by the runtime from the log and the policy</b>",
|
|
1132
|
-
...stale.lines.map((line) => `• ${escapeHtml(line)}`),
|
|
1133
|
-
...digest.facts.map((fact) => `• <b>${escapeHtml(fact.label)}:</b> ${escapeHtml(fact.text)} <i>(${escapeHtml(fact.origin)})</i>`),
|
|
1191
|
+
`<b>${escapeHtml(headline)}</b>`,
|
|
1192
|
+
...stale.lines.map((line) => escapeHtml(line)),
|
|
1193
|
+
...reminder.slice(1).map((line) => escapeHtml(line)),
|
|
1134
1194
|
"",
|
|
1135
|
-
`<b
|
|
1195
|
+
`<b>${escapeHtml(claimedLabelFor(digest.author))}</b>`,
|
|
1136
1196
|
];
|
|
1137
1197
|
for (const [index, member] of digest.members.entries()) {
|
|
1138
|
-
|
|
1198
|
+
const age = stale.ages?.[index];
|
|
1199
|
+
lines.push(`${index + 1}. ${escapeHtml(member.summary)}${age === undefined ? "" : ` · ${escapeHtml(age)}`}`);
|
|
1139
1200
|
if (member.settled !== null) {
|
|
1140
1201
|
lines.push(` <b>${escapeHtml(member.settled.headline)}</b>`);
|
|
1141
1202
|
for (const detail of member.settled.detail)
|
|
1142
1203
|
lines.push(` ${escapeHtml(detail)}`);
|
|
1143
1204
|
}
|
|
1144
1205
|
}
|
|
1145
|
-
|
|
1206
|
+
if (open > 0) {
|
|
1207
|
+
const approve = stale.staleCopy === false
|
|
1208
|
+
? "To approve one, ask your agent to try again."
|
|
1209
|
+
: "To approve one, use its earlier message if you still have it, or ask your agent to try again.";
|
|
1210
|
+
lines.push("", escapeHtml(`This message does not show what they would send, so it has no approve button. ${approve} Reject all turns down only ${open === 1 ? "this one" : `these ${String(open)}`}.`));
|
|
1211
|
+
}
|
|
1146
1212
|
const rows = open === 0
|
|
1147
1213
|
? []
|
|
1148
1214
|
: [
|
|
@@ -1161,10 +1227,10 @@ function renderStaleSummary(digest, stale, open, total) {
|
|
|
1161
1227
|
/** The digest's headline, given how much of it is still open. */
|
|
1162
1228
|
function digestHeadline(open, total) {
|
|
1163
1229
|
if (open === 0)
|
|
1164
|
-
return `
|
|
1230
|
+
return `All ${total} requests decided`;
|
|
1165
1231
|
if (open === total)
|
|
1166
|
-
return `${total}
|
|
1167
|
-
return `${open}
|
|
1232
|
+
return `${total} requests are waiting for you`;
|
|
1233
|
+
return `${open} of ${total} requests still waiting for you`;
|
|
1168
1234
|
}
|
|
1169
1235
|
/**
|
|
1170
1236
|
* The digest message: text plus the keyboard for whatever is still open.
|
|
@@ -1186,23 +1252,28 @@ export function renderDigest(digest) {
|
|
|
1186
1252
|
const stale = digest.stale ?? null;
|
|
1187
1253
|
if (stale !== null)
|
|
1188
1254
|
return renderStaleSummary(digest, stale, open.length, total);
|
|
1255
|
+
// P-9: a headline, one computed line, then the agent's own words under a
|
|
1256
|
+
// label that says they are the agent's and unchecked. No action key: the
|
|
1257
|
+
// members are told apart by number, and the numbers are on the buttons.
|
|
1189
1258
|
const lines = [
|
|
1190
1259
|
`<b>${escapeHtml(digestHeadline(open.length, total))}</b>`,
|
|
1260
|
+
escapeHtml(digest.line),
|
|
1191
1261
|
"",
|
|
1192
|
-
|
|
1193
|
-
...digest.facts.map((fact) => `• <b>${escapeHtml(fact.label)}:</b> ${escapeHtml(fact.text)} <i>(${escapeHtml(fact.origin)})</i>`),
|
|
1194
|
-
"",
|
|
1195
|
-
`<b>CLAIMED — authored by ${escapeHtml(digest.author)}, NOT verified by the runtime</b>`,
|
|
1262
|
+
`<b>${escapeHtml(claimedLabelFor(digest.author))}</b>`,
|
|
1196
1263
|
];
|
|
1197
1264
|
for (const [index, member] of digest.members.entries()) {
|
|
1198
|
-
lines.push(`${index + 1}.
|
|
1265
|
+
lines.push(`${index + 1}. ${member.kind === undefined ? "" : `${escapeHtml(member.kind)} — `}${escapeHtml(member.summary)} · agent's estimate ${escapeHtml(member.cost)}`);
|
|
1199
1266
|
if (member.settled !== null) {
|
|
1200
1267
|
lines.push(` <b>${escapeHtml(member.settled.headline)}</b>`);
|
|
1201
1268
|
for (const detail of member.settled.detail)
|
|
1202
1269
|
lines.push(` ${escapeHtml(detail)}`);
|
|
1203
1270
|
}
|
|
1204
1271
|
}
|
|
1205
|
-
|
|
1272
|
+
if (open.length > 0) {
|
|
1273
|
+
lines.push("", escapeHtml(open.length > 1
|
|
1274
|
+
? `Each button answers one request, by number. "All" answers every request still open.`
|
|
1275
|
+
: "Each button answers one request, by number."));
|
|
1276
|
+
}
|
|
1206
1277
|
const rows = [];
|
|
1207
1278
|
for (const [index, member] of digest.members.entries()) {
|
|
1208
1279
|
if (member.settled !== null)
|
|
@@ -2498,7 +2569,10 @@ export class TelegramChannel {
|
|
|
2498
2569
|
async notifyStale(members, stale) {
|
|
2499
2570
|
if (members.length === 0)
|
|
2500
2571
|
return null;
|
|
2501
|
-
return this.deliverDigest(members, `tg-batch-${this.makeNonce()}`,
|
|
2572
|
+
return this.deliverDigest(members, `tg-batch-${this.makeNonce()}`, {
|
|
2573
|
+
...stale,
|
|
2574
|
+
staleCopy: stale.staleCopy ?? this.staleCopy,
|
|
2575
|
+
});
|
|
2502
2576
|
}
|
|
2503
2577
|
/**
|
|
2504
2578
|
* The digest itself: every member's prompt and payload, then the one message
|
|
@@ -2520,6 +2594,7 @@ export class TelegramChannel {
|
|
|
2520
2594
|
const windowMs = windows.includes(null)
|
|
2521
2595
|
? null
|
|
2522
2596
|
: Math.max(...windows.filter((window) => window !== null));
|
|
2597
|
+
const mixedClasses = new Set(members.map((member) => member.class.value)).size > 1;
|
|
2523
2598
|
const state = {
|
|
2524
2599
|
deliveredAtMs,
|
|
2525
2600
|
windowMs,
|
|
@@ -2528,13 +2603,14 @@ export class TelegramChannel {
|
|
|
2528
2603
|
batchDeliveryId,
|
|
2529
2604
|
allNonce,
|
|
2530
2605
|
stale,
|
|
2531
|
-
|
|
2606
|
+
line: stale === null ? digestLine(members) : "",
|
|
2532
2607
|
author: originOf(members[0].summary),
|
|
2533
2608
|
members: members.map((member) => ({
|
|
2534
2609
|
actionKey: member.action_key.value,
|
|
2535
2610
|
nonce: this.makeNonce(),
|
|
2536
2611
|
summary: member.summary.value ?? "(none given)",
|
|
2537
2612
|
cost: `$${member.est_cost_usd.value.toFixed(2)}`,
|
|
2613
|
+
...(mixedClasses ? { kind: member.class.value } : {}),
|
|
2538
2614
|
settled: null,
|
|
2539
2615
|
})),
|
|
2540
2616
|
};
|
|
@@ -2557,12 +2633,12 @@ export class TelegramChannel {
|
|
|
2557
2633
|
}
|
|
2558
2634
|
else if (shared === null) {
|
|
2559
2635
|
for (const [index, member] of members.entries()) {
|
|
2560
|
-
const one = await this.sendPrompt(member, `
|
|
2636
|
+
const one = await this.sendPrompt(member, `Request ${index + 1} of ${members.length} — answer it on the summary below`, null);
|
|
2561
2637
|
rendered.push({ ...one.rendered, batchDeliveryId });
|
|
2562
2638
|
}
|
|
2563
2639
|
}
|
|
2564
2640
|
else {
|
|
2565
|
-
const one = await this.sendPrompt(shared, `
|
|
2641
|
+
const one = await this.sendPrompt(shared, `What all ${members.length} requests are about — answer them on the summary below`, null);
|
|
2566
2642
|
for (const member of members) {
|
|
2567
2643
|
rendered.push({
|
|
2568
2644
|
...one.rendered,
|
|
@@ -2631,7 +2707,7 @@ export class TelegramChannel {
|
|
|
2631
2707
|
for (const [index, chunk] of chunks.entries()) {
|
|
2632
2708
|
const label = chunks.length === 1
|
|
2633
2709
|
? `<b>${PAYLOAD_CHUNK_LABEL}</b>`
|
|
2634
|
-
: `<b
|
|
2710
|
+
: `<b>${PAYLOAD_CHUNK_LABEL} (part ${index + 1} of ${chunks.length})</b>`;
|
|
2635
2711
|
segments.push(`${label}\n<pre>${escapeHtml(chunk)}</pre>`);
|
|
2636
2712
|
}
|
|
2637
2713
|
}
|