@primusclaw/personal-agent 0.4.0 → 0.6.0
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/dist/index.js +1025 -612
- package/dist/types/approvals/questions.d.ts +7 -0
- package/dist/types/approvals/records.d.ts +2 -2
- package/dist/types/approvals/views.d.ts +8 -0
- package/dist/types/assistant/chat.d.ts +2 -0
- package/dist/types/assistant/choice-clicks.d.ts +35 -0
- package/dist/types/assistant/events.d.ts +17 -0
- package/dist/types/assistant/index.d.ts +1 -0
- package/dist/types/assistant/receipts.d.ts +1 -1
- package/dist/types/assistant/say.d.ts +12 -4
- package/dist/types/assistant/views.d.ts +22 -0
- package/dist/types/language/chosen.d.ts +23 -0
- package/dist/types/locales/approval.d.ts +11 -0
- package/dist/types/locales/assistant-cards.d.ts +15 -0
- package/dist/types/locales/turn.d.ts +2 -0
- package/dist/types/ports/store/approvals.d.ts +24 -2
- package/dist/types/ports/store/assistant.d.ts +27 -1
- package/dist/types/ports/surface.d.ts +11 -2
- package/dist/types/protocol/claw-api/assistant-events.d.ts +11 -0
- package/dist/types/routing/credentials.d.ts +9 -1
- package/dist/types/routing/deps.d.ts +1 -1
- package/dist/types/routing/personal-text.d.ts +3 -2
- package/dist/types/routing/question-reply.d.ts +22 -0
- package/dist/types/routing/turn-runner.d.ts +5 -0
- package/dist/types/routing/turn-start.d.ts +22 -1
- package/dist/types/turn/engine/context.d.ts +8 -4
- package/dist/types/turn/engine/run-turn.d.ts +2 -0
- package/dist/types/turn/progress/status-message.d.ts +20 -0
- package/dist/types/turn/runner.d.ts +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3800,7 +3800,14 @@ var approvalText = defineCatalog({
|
|
|
3800
3800
|
questionYours: "Your answer",
|
|
3801
3801
|
questionOnly: (p) => `Only ${p.who} can answer.`,
|
|
3802
3802
|
questionAsker: "the person who asked",
|
|
3803
|
-
send: "Send answer"
|
|
3803
|
+
send: "Send answer",
|
|
3804
|
+
/** Under a question card's questions: a client that does not show the card can still answer. */
|
|
3805
|
+
questionReply: "You can also just reply in the chat with your choice.",
|
|
3806
|
+
/** The same in a group chat, where only a message that @mentions the bot reaches it. */
|
|
3807
|
+
questionReplyGroup: "You can also just reply in the chat with your choice, @mentioning me.",
|
|
3808
|
+
/** A question card once a message typed in the chat answered it. */
|
|
3809
|
+
questionReplied: (p) => `Answered: ${p.answer}`,
|
|
3810
|
+
questionRepliedAt: (p) => `Replied in the chat, ${p.at}`
|
|
3804
3811
|
},
|
|
3805
3812
|
zh: {
|
|
3806
3813
|
title: "\u9700\u8981\u4F60\u6279\u51C6",
|
|
@@ -3823,7 +3830,11 @@ var approvalText = defineCatalog({
|
|
|
3823
3830
|
questionYours: "\u4F60\u7684\u56DE\u7B54",
|
|
3824
3831
|
questionOnly: (p) => `\u53EA\u6709${p.who}\u53EF\u4EE5\u56DE\u7B54\u3002`,
|
|
3825
3832
|
questionAsker: "\u53D1\u8D77\u4EBA",
|
|
3826
|
-
send: "\u53D1\u9001\u56DE\u7B54"
|
|
3833
|
+
send: "\u53D1\u9001\u56DE\u7B54",
|
|
3834
|
+
questionReply: "\u4E5F\u53EF\u4EE5\u76F4\u63A5\u5728\u804A\u5929\u91CC\u56DE\u590D\u9009\u9879\u3002",
|
|
3835
|
+
questionReplyGroup: "\u4E5F\u53EF\u4EE5\u5728\u804A\u5929\u91CC @\u6211 \u76F4\u63A5\u56DE\u590D\u9009\u9879\u3002",
|
|
3836
|
+
questionReplied: (p) => `\u5DF2\u56DE\u7B54\uFF1A${p.answer}`,
|
|
3837
|
+
questionRepliedAt: (p) => `\u5728\u804A\u5929\u91CC\u56DE\u590D\uFF0C${p.at}`
|
|
3827
3838
|
}
|
|
3828
3839
|
});
|
|
3829
3840
|
|
|
@@ -4018,7 +4029,18 @@ var assistantCardText = defineCatalog({
|
|
|
4018
4029
|
minutesAgo: (p) => `${String(p.n)} min ago`,
|
|
4019
4030
|
hoursAgo: (p) => `${String(p.n)} h ago`,
|
|
4020
4031
|
daysAgo: (p) => `${String(p.n)} d ago`,
|
|
4021
|
-
askedTitle: (p) => `Asked of others \xB7 ${String(p.n)}
|
|
4032
|
+
askedTitle: (p) => `Asked of others \xB7 ${String(p.n)}`,
|
|
4033
|
+
choiceTitle: "Pick one",
|
|
4034
|
+
choiceTitleMany: "Pick any that apply",
|
|
4035
|
+
choiceSend: "Send",
|
|
4036
|
+
choiceTyped: "You can also just reply in the chat.",
|
|
4037
|
+
choiceNone: "Pick an option first, then press Send.",
|
|
4038
|
+
choiceSent: (p) => `You picked: ${p.answer}`,
|
|
4039
|
+
choiceGone: "This choice is no longer open. Reply in the chat instead.",
|
|
4040
|
+
choiceSending: (p) => `Sending: ${p.answer}. If nothing comes back, reply in the chat instead.`,
|
|
4041
|
+
choiceFailed: "That did not go through. Press Send again.",
|
|
4042
|
+
/** How the options picked are joined into the person's message. */
|
|
4043
|
+
choiceJoin: ", "
|
|
4022
4044
|
},
|
|
4023
4045
|
zh: {
|
|
4024
4046
|
...english2,
|
|
@@ -4055,7 +4077,17 @@ var assistantCardText = defineCatalog({
|
|
|
4055
4077
|
minutesAgo: (p) => `${String(p.n)} \u5206\u949F\u524D`,
|
|
4056
4078
|
hoursAgo: (p) => `${String(p.n)} \u5C0F\u65F6\u524D`,
|
|
4057
4079
|
daysAgo: (p) => `${String(p.n)} \u5929\u524D`,
|
|
4058
|
-
askedTitle: (p) => `\u8BF7\u522B\u4EBA\u505A\u7684\u4EE3\u5BA1 \xB7 ${String(p.n)}
|
|
4080
|
+
askedTitle: (p) => `\u8BF7\u522B\u4EBA\u505A\u7684\u4EE3\u5BA1 \xB7 ${String(p.n)}`,
|
|
4081
|
+
choiceTitle: "\u8BF7\u9009\u62E9\u4E00\u9879",
|
|
4082
|
+
choiceTitleMany: "\u53EF\u591A\u9009",
|
|
4083
|
+
choiceSend: "\u53D1\u9001",
|
|
4084
|
+
choiceTyped: "\u4E5F\u53EF\u4EE5\u76F4\u63A5\u5728\u804A\u5929\u91CC\u56DE\u590D\u3002",
|
|
4085
|
+
choiceNone: "\u8BF7\u5148\u9009\u4E00\u9879\uFF0C\u518D\u70B9\u53D1\u9001\u3002",
|
|
4086
|
+
choiceSent: (p) => `\u4F60\u9009\u4E86\uFF1A${p.answer}`,
|
|
4087
|
+
choiceGone: "\u8FD9\u4E2A\u9009\u62E9\u5DF2\u7ECF\u5931\u6548\uFF0C\u8BF7\u76F4\u63A5\u5728\u804A\u5929\u91CC\u56DE\u590D\u3002",
|
|
4088
|
+
choiceSending: (p) => `\u6B63\u5728\u53D1\u9001\uFF1A${p.answer}\u3002\u5982\u679C\u4E00\u76F4\u6CA1\u6709\u56DE\u5E94\uFF0C\u8BF7\u76F4\u63A5\u5728\u804A\u5929\u91CC\u56DE\u590D\u3002`,
|
|
4089
|
+
choiceFailed: "\u6CA1\u6709\u53D1\u51FA\u53BB\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u53D1\u9001\u3002",
|
|
4090
|
+
choiceJoin: "\u3001"
|
|
4059
4091
|
}
|
|
4060
4092
|
});
|
|
4061
4093
|
var ASKED_VERDICT = {
|
|
@@ -5746,6 +5778,8 @@ var turnText = defineCatalog({
|
|
|
5746
5778
|
thinking: "Thinking\u2026",
|
|
5747
5779
|
/** A turn still running when its stream has to close: the answer follows as a message of its own. */
|
|
5748
5780
|
stillWorking: "Still working on it; the result will follow as a new message.",
|
|
5781
|
+
/** A turn whose stream is closed because a card now waits for the person's choice. */
|
|
5782
|
+
awaitingChoice: "Waiting for your choice in the card; I will continue once you pick.",
|
|
5749
5783
|
interrupted: "Interrupted.",
|
|
5750
5784
|
/** What a turn the person stopped ends with. */
|
|
5751
5785
|
stopped: "Stopped.",
|
|
@@ -5767,6 +5801,7 @@ var turnText = defineCatalog({
|
|
|
5767
5801
|
planMore: (p) => `\u2026\u2026\u53E6\u6709 ${String(p.n)} \u9879\uFF08${String(p.done)} \u9879\u5DF2\u5B8C\u6210\uFF0C${String(p.todo)} \u9879\u5F85\u529E\uFF09`,
|
|
5768
5802
|
thinking: "\u601D\u8003\u4E2D\u2026",
|
|
5769
5803
|
stillWorking: "\u4ECD\u5728\u5904\u7406\uFF0C\u5B8C\u6210\u540E\u53E6\u53D1\u7ED3\u679C\u3002",
|
|
5804
|
+
awaitingChoice: "\u8BF7\u5728\u5361\u7247\u91CC\u9009\u62E9\uFF0C\u9009\u597D\u540E\u6211\u63A5\u7740\u505A\u3002",
|
|
5770
5805
|
interrupted: "\u5DF2\u4E2D\u65AD\u3002",
|
|
5771
5806
|
stopped: "\u5DF2\u505C\u6B62\u3002",
|
|
5772
5807
|
doneShort: "\u5B8C\u6210\u3002",
|
|
@@ -6062,6 +6097,19 @@ function deliveryLine(delivery, url) {
|
|
|
6062
6097
|
return turnEnglish.deliveryLine(plain(delivery.name), sizeText(delivery.size), caption, url);
|
|
6063
6098
|
}
|
|
6064
6099
|
|
|
6100
|
+
// packages/core/src/language/chosen.ts
|
|
6101
|
+
function chosenLang(prefs) {
|
|
6102
|
+
if (!prefs.langSet) return void 0;
|
|
6103
|
+
return prefs.lang === "zh" || prefs.lang === "en" ? prefs.lang : void 0;
|
|
6104
|
+
}
|
|
6105
|
+
function replyLanguageContext(prefs) {
|
|
6106
|
+
const lang = chosenLang(prefs);
|
|
6107
|
+
return lang === void 0 ? {} : { reply_language: lang };
|
|
6108
|
+
}
|
|
6109
|
+
function replyLanguageLine(lang) {
|
|
6110
|
+
return `[Reply language: ${lang === "zh" ? "Chinese" : "English"}]`;
|
|
6111
|
+
}
|
|
6112
|
+
|
|
6065
6113
|
// packages/core/src/language/detect.ts
|
|
6066
6114
|
var CJK2 = /[㐀-䶿一-鿿豈-]/u;
|
|
6067
6115
|
function hasCjk2(text8) {
|
|
@@ -6195,9 +6243,13 @@ ${text8}` : quote || text8;
|
|
|
6195
6243
|
function messageOf(ctx) {
|
|
6196
6244
|
const said2 = withQuote(ctx.request.sent || ctx.request.text, ctx.request.quote ?? "");
|
|
6197
6245
|
const note = ctx.record.filesNote;
|
|
6198
|
-
|
|
6246
|
+
const message = said2 && note ? `${said2}
|
|
6199
6247
|
|
|
6200
6248
|
${note}` : said2 || note;
|
|
6249
|
+
const chosen = ctx.request.replyLang;
|
|
6250
|
+
return message && chosen !== void 0 ? `${message}
|
|
6251
|
+
|
|
6252
|
+
${replyLanguageLine(chosen)}` : message;
|
|
6201
6253
|
}
|
|
6202
6254
|
|
|
6203
6255
|
// packages/core/src/locales/claw.ts
|
|
@@ -6949,6 +7001,8 @@ var StatusMessage = class {
|
|
|
6949
7001
|
opening;
|
|
6950
7002
|
expiry;
|
|
6951
7003
|
again;
|
|
7004
|
+
/** The stream being closed because a card waits for the person (`awaitingChoice`). */
|
|
7005
|
+
choice;
|
|
6952
7006
|
preparingTimer;
|
|
6953
7007
|
lastText = "";
|
|
6954
7008
|
lastAt = 0;
|
|
@@ -7027,20 +7081,61 @@ ${header}` : header;
|
|
|
7027
7081
|
const live = this.live;
|
|
7028
7082
|
if (!live) return;
|
|
7029
7083
|
await sleepSeconds(this.clock, Math.max(STREAM_CLOSE_SECONDS - live.age(), 0), signal);
|
|
7030
|
-
|
|
7084
|
+
await this.closeEarly(live, turnText.in(this.lang).stillWorking, "before the chat's limit", signal);
|
|
7085
|
+
}
|
|
7086
|
+
/**
|
|
7087
|
+
* Close the open stream with `line` (after the agent's words in the bubble) while the turn goes on; the answer, whenever it comes, is
|
|
7088
|
+
* a message of its own. Nothing more is sent to the stream once this has begun.
|
|
7089
|
+
*/
|
|
7090
|
+
async closeEarly(live, line2, why, signal) {
|
|
7091
|
+
if (this.closed || !live.closable || this.streamOver || this.expiring) return;
|
|
7031
7092
|
this.expiring = true;
|
|
7032
7093
|
if (this.stream && this.flusher && !this.flusher.done) await unlessAborted(this.flusher.settled(), signal);
|
|
7033
7094
|
this.streamOver = true;
|
|
7034
|
-
const
|
|
7035
|
-
const closed = await live.final(plainMessage(
|
|
7095
|
+
const words3 = this.streamable() || live.streamed;
|
|
7096
|
+
const closed = await live.final(plainMessage(words3 ? `${words3}
|
|
7036
7097
|
|
|
7037
7098
|
${line2}` : line2));
|
|
7038
|
-
if (!closed.ok)
|
|
7039
|
-
this.log.warn("the turn's stream could not be closed before the chat's limit; the answer follows as a message");
|
|
7099
|
+
if (!closed.ok) this.log.warn(`the turn's stream could not be closed ${why}; the answer follows as a message`);
|
|
7040
7100
|
this.log.info("the turn's stream was closed; the answer follows as a message of its own", {
|
|
7101
|
+
why,
|
|
7041
7102
|
age: Math.round(live.age())
|
|
7042
7103
|
});
|
|
7043
7104
|
}
|
|
7105
|
+
/**
|
|
7106
|
+
* A card now waits for the person (a question of the agent): it is their turn, so the floating status must not go on
|
|
7107
|
+
* saying "Thinking…" until the chat's limit. An open stream is closed at once with AWAITING_CHOICE, and the answer comes
|
|
7108
|
+
* as a message of its own, as after the limit. Without a stream (the progress message) nothing changes: it has no
|
|
7109
|
+
* spinner. Never fails the turn.
|
|
7110
|
+
*/
|
|
7111
|
+
awaitingChoice() {
|
|
7112
|
+
const live = this.live;
|
|
7113
|
+
if (this.closed || !live || this.streamOver || this.expiring || this.choice) return;
|
|
7114
|
+
this.choice = detach(async (signal) => {
|
|
7115
|
+
if (this.opening && !this.opening.done) await unlessAborted(this.opening.settled(), signal);
|
|
7116
|
+
if (!this.expiring) this.expiry?.cancel();
|
|
7117
|
+
await this.closeEarly(live, turnText.in(this.lang).awaitingChoice, "while a card waits for the person", signal);
|
|
7118
|
+
});
|
|
7119
|
+
}
|
|
7120
|
+
/**
|
|
7121
|
+
* Claw queued the message behind a turn it is still running: `notice` closes the stream at once, so it does not say
|
|
7122
|
+
* "Thinking…" for the whole wait. The answer comes as a message of its own, as after the limit. When the stream did not open
|
|
7123
|
+
* after all, `notice` is said as a message. False when there is no stream: the caller says it. Never fails the turn.
|
|
7124
|
+
*/
|
|
7125
|
+
queued(notice) {
|
|
7126
|
+
const live = this.live;
|
|
7127
|
+
if (this.closed || !live || this.streamOver || this.expiring || this.choice) return false;
|
|
7128
|
+
this.choice = detach(async (signal) => {
|
|
7129
|
+
if (this.opening && !this.opening.done) await unlessAborted(this.opening.settled(), signal);
|
|
7130
|
+
if (this.closed || !live.closable || this.streamOver || this.expiring) {
|
|
7131
|
+
await this.surface.send(plainMessage(notice));
|
|
7132
|
+
return;
|
|
7133
|
+
}
|
|
7134
|
+
this.expiry?.cancel();
|
|
7135
|
+
await this.closeEarly(live, notice, "while the message waits behind another turn", signal);
|
|
7136
|
+
});
|
|
7137
|
+
return true;
|
|
7138
|
+
}
|
|
7044
7139
|
/** The chat's id of the posted message; undefined when the post failed or its id never came (nothing is edited then). */
|
|
7045
7140
|
async messageId(signal) {
|
|
7046
7141
|
if (!this.posted) return void 0;
|
|
@@ -7185,8 +7280,7 @@ ${line2}` : line2));
|
|
|
7185
7280
|
this.hard = this.hard || hard;
|
|
7186
7281
|
this.stopPreparingTimer();
|
|
7187
7282
|
if (this.flusher && !this.flusher.done && (hard || !this.sending)) this.flusher.cancel();
|
|
7188
|
-
if (hard) this.again?.cancel();
|
|
7189
|
-
if (hard) this.expiry?.cancel();
|
|
7283
|
+
if (hard) for (const task of [this.again, this.expiry, this.choice]) task?.cancel();
|
|
7190
7284
|
if (hard && this.live?.closable && !this.streamOver) {
|
|
7191
7285
|
this.streamOver = true;
|
|
7192
7286
|
const live = this.live;
|
|
@@ -7220,7 +7314,7 @@ ${line2}` : line2));
|
|
|
7220
7314
|
async quiesce() {
|
|
7221
7315
|
this.close();
|
|
7222
7316
|
if (this.expiry && !this.expiry.done && !this.expiring) this.expiry.cancel();
|
|
7223
|
-
for (const task of [this.opening, this.flusher, this.expiring ? this.expiry : void 0]) {
|
|
7317
|
+
for (const task of [this.opening, this.flusher, this.expiring ? this.expiry : void 0, this.choice]) {
|
|
7224
7318
|
if (task) await task.settled();
|
|
7225
7319
|
}
|
|
7226
7320
|
}
|
|
@@ -7992,15 +8086,17 @@ async function ask2(ctx, sessionId, step) {
|
|
|
7992
8086
|
const asked3 = isObject3(step.approval) && truthy(step.approval) ? approvalOf(ctx, sessionId, step.approval) : questionOf2(ctx, sessionId, step.question ?? {});
|
|
7993
8087
|
const { tool: tool2, toolUseId, request, envelope } = asked3;
|
|
7994
8088
|
const hooks = ctx.hooks;
|
|
7995
|
-
|
|
8089
|
+
const questions = tool2 === QUESTION_TOOL ? request["questions"] : void 0;
|
|
8090
|
+
if (toolUseId && hooks.onApproval?.(sessionId, toolUseId, tool2, waitSeconds(request["timeout_ms"]), envelope, questions) === false)
|
|
7996
8091
|
return;
|
|
8092
|
+
let cardId;
|
|
7997
8093
|
try {
|
|
7998
|
-
await ctx.deps.surface.deliver(envelope);
|
|
8094
|
+
cardId = await ctx.deps.surface.deliver(envelope);
|
|
7999
8095
|
} catch (err2) {
|
|
8000
8096
|
if (toolUseId) hooks.onApprovalUnsent?.(sessionId, toolUseId);
|
|
8001
8097
|
throw err2;
|
|
8002
8098
|
}
|
|
8003
|
-
if (toolUseId) hooks.onApprovalSent?.(sessionId, toolUseId);
|
|
8099
|
+
if (toolUseId) hooks.onApprovalSent?.(sessionId, toolUseId, cardId);
|
|
8004
8100
|
}
|
|
8005
8101
|
async function readOnce(ctx, args, cursor, told) {
|
|
8006
8102
|
const { sessionId, state, status, signal } = args;
|
|
@@ -8032,6 +8128,7 @@ async function take(ctx, args, step) {
|
|
|
8032
8128
|
args.status.progress(step);
|
|
8033
8129
|
if (truthy(step.approval) || truthy(step.question)) {
|
|
8034
8130
|
await ask2(ctx, args.sessionId, step);
|
|
8131
|
+
if (!truthy(step.approval)) args.status.awaitingChoice();
|
|
8035
8132
|
return void 0;
|
|
8036
8133
|
}
|
|
8037
8134
|
if (!step.finished) return void 0;
|
|
@@ -8418,8 +8515,9 @@ var TurnRun = class {
|
|
|
8418
8515
|
state.writes = ctx.record.wrote;
|
|
8419
8516
|
this.state = state;
|
|
8420
8517
|
noteRunning(ctx, state, this.running);
|
|
8421
|
-
if (messageId
|
|
8422
|
-
|
|
8518
|
+
if (queuedByClaw(messageId) && !ctx.request.attach) {
|
|
8519
|
+
const notice = turnText.in(langOf(ctx)).queuedNotice;
|
|
8520
|
+
if (!this.status.queued(notice)) await ctx.deps.surface.send(plainMessage(notice));
|
|
8423
8521
|
}
|
|
8424
8522
|
return consume(ctx, { sessionId, state, status: this.status, running: this.running, signal });
|
|
8425
8523
|
}
|
|
@@ -8573,6 +8671,10 @@ var TurnRun = class {
|
|
|
8573
8671
|
await offerReport(ctx);
|
|
8574
8672
|
}
|
|
8575
8673
|
};
|
|
8674
|
+
var QUEUED_MESSAGE_PREFIX = "claw-pending-";
|
|
8675
|
+
function queuedByClaw(messageId) {
|
|
8676
|
+
return messageId === void 0 || messageId.startsWith(QUEUED_MESSAGE_PREFIX);
|
|
8677
|
+
}
|
|
8576
8678
|
async function runTurn(ctx, controls) {
|
|
8577
8679
|
const replyTo = streamReplyTo(ctx);
|
|
8578
8680
|
if (!replyTo) detach(() => typing(ctx));
|
|
@@ -8773,6 +8875,7 @@ var TurnEngine = class {
|
|
|
8773
8875
|
quote: run.quote,
|
|
8774
8876
|
sent: run.sent,
|
|
8775
8877
|
lang: run.lang,
|
|
8878
|
+
...run.replyLang === void 0 ? {} : { replyLang: run.replyLang },
|
|
8776
8879
|
files: run.files,
|
|
8777
8880
|
...review ? reviewRequest(review) : {}
|
|
8778
8881
|
};
|
|
@@ -8782,9 +8885,9 @@ var TurnEngine = class {
|
|
|
8782
8885
|
const review = run.review?.turn;
|
|
8783
8886
|
return {
|
|
8784
8887
|
...run.provider ? { keyProvider: run.provider } : {},
|
|
8785
|
-
onApproval: (sid, toolUseId, tool2, expiresIn, envelope) => approvals.record(run.userId, sid, toolUseId, tool2, expiresIn, envelope),
|
|
8786
|
-
onApprovalSent: (sid, toolUseId) => {
|
|
8787
|
-
approvals.sent(sid, toolUseId);
|
|
8888
|
+
onApproval: (sid, toolUseId, tool2, expiresIn, envelope, questions) => approvals.record(run.userId, sid, toolUseId, tool2, expiresIn, envelope, questions),
|
|
8889
|
+
onApprovalSent: (sid, toolUseId, cardId) => {
|
|
8890
|
+
approvals.sent(sid, toolUseId, cardId);
|
|
8788
8891
|
},
|
|
8789
8892
|
onApprovalUnsent: (sid, toolUseId) => {
|
|
8790
8893
|
approvals.unsent(sid, toolUseId);
|
|
@@ -9697,6 +9800,14 @@ var CredentialUnavailable = class extends Error {
|
|
|
9697
9800
|
};
|
|
9698
9801
|
var IDENTITY_CARD_INTERVAL_SECONDS = 24 * 3600;
|
|
9699
9802
|
var IDENTITY_CARD_NOTICE = "identity_card";
|
|
9803
|
+
function chatCredentialOf(links2, exchange) {
|
|
9804
|
+
return async (sender) => {
|
|
9805
|
+
const key = links2.getKey(sender.id) ?? void 0;
|
|
9806
|
+
if (exchange === void 0 || !exchange.enabled) return key;
|
|
9807
|
+
const token = await exchange.tokenFor(sender.id);
|
|
9808
|
+
return token.ok ? token.value : key;
|
|
9809
|
+
};
|
|
9810
|
+
}
|
|
9700
9811
|
var Credentials = class {
|
|
9701
9812
|
constructor(deps, views) {
|
|
9702
9813
|
this.deps = deps;
|
|
@@ -10203,7 +10314,7 @@ var CHAT_ENTRIES = [
|
|
|
10203
10314
|
category: "chat",
|
|
10204
10315
|
zh: "\u8981\u4F60\u62CD\u677F\u65F6\u6211\u53D1\u5361\u7247\uFF1A\u70B9\u6309\u94AE\u56DE\u7B54\u3001\u6279\u51C6\u6216\u62D2\u7EDD",
|
|
10205
10316
|
en: "When I need you, I send a card: tap to answer, approve or deny",
|
|
10206
|
-
covers: ["card:decision", "card:answer", "card:assistant_action"],
|
|
10317
|
+
covers: ["card:decision", "card:answer", "card:assistant_action", "card:assistant_choice"],
|
|
10207
10318
|
where: "both"
|
|
10208
10319
|
}),
|
|
10209
10320
|
capability({
|
|
@@ -13134,11 +13245,12 @@ var PersonalRouter = class {
|
|
|
13134
13245
|
const { assistant, remote } = this.ctx.deps;
|
|
13135
13246
|
const userId = chat.sender.id;
|
|
13136
13247
|
const understood = assistant.understands(userId);
|
|
13137
|
-
const
|
|
13138
|
-
const
|
|
13139
|
-
const
|
|
13248
|
+
const choice = message.choice === true;
|
|
13249
|
+
const switching = understood && !choice && remote.enabled && switchCommand(message.text) !== null;
|
|
13250
|
+
const forAssistant = understood && (choice || !exactCommand(message.text, { menuCommand, isKnownCommand })) && !switching;
|
|
13251
|
+
const text8 = understood && !choice ? menuCommand(message.text) : this.text.textOf(message);
|
|
13140
13252
|
learnLang(this.ctx.deps.store, userId, message.text);
|
|
13141
|
-
if (await this.beforeCredential(message, chat, text8, forAssistant)) return;
|
|
13253
|
+
if (!choice && await this.beforeCredential(message, chat, text8, forAssistant)) return;
|
|
13142
13254
|
const key = await this.ctx.credentials.credential(chat, true);
|
|
13143
13255
|
if (key === void 0) return;
|
|
13144
13256
|
if (forAssistant && !this.text.talksToAgent(message)) await this.text.understand(message, chat, key);
|
|
@@ -13234,7 +13346,7 @@ var GroupRemote = class {
|
|
|
13234
13346
|
const { remote } = this.ctx.deps;
|
|
13235
13347
|
const bound = this.chatOf(chat);
|
|
13236
13348
|
const attached = remote.attached(bound.id);
|
|
13237
|
-
const args = this.agentArgs(words3.text, attached);
|
|
13349
|
+
const args = message.choice === true ? null : this.agentArgs(words3.text, attached);
|
|
13238
13350
|
const provider = this.ctx.credentials.provider(chat.sender.id, key);
|
|
13239
13351
|
if (args !== null) {
|
|
13240
13352
|
const parsed = parseAgentCommand(args);
|
|
@@ -13255,11 +13367,12 @@ var GroupRemote = class {
|
|
|
13255
13367
|
const { remote } = this.ctx.deps;
|
|
13256
13368
|
const bound = this.chatOf(chat);
|
|
13257
13369
|
const provider = this.ctx.credentials.provider(chat.sender.id, key);
|
|
13258
|
-
|
|
13370
|
+
const choice = message.choice === true;
|
|
13371
|
+
if (!choice && (naturalCommand(words3.text) === "/stop" || parseCommand(words3.text) === "stop")) {
|
|
13259
13372
|
await remote.stop(bound, key, chat.conversation);
|
|
13260
13373
|
return true;
|
|
13261
13374
|
}
|
|
13262
|
-
if (parseCommand(words3.text) !== null) return false;
|
|
13375
|
+
if (!choice && parseCommand(words3.text) !== null) return false;
|
|
13263
13376
|
let quote = words3.quote;
|
|
13264
13377
|
if (remote.windowMode(bound.id) === "card") {
|
|
13265
13378
|
if (!remote.quotesWindow(bound.id, message)) return false;
|
|
@@ -13273,160 +13386,687 @@ var GroupRemote = class {
|
|
|
13273
13386
|
}
|
|
13274
13387
|
};
|
|
13275
13388
|
|
|
13276
|
-
// packages/core/src/
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
textOf(message, group = false) {
|
|
13292
|
-
const { github, assistant, remote, config } = this.ctx.deps;
|
|
13293
|
-
const userId = group ? void 0 : message.sender.id;
|
|
13294
|
-
const on = (test) => userId !== void 0 && test(userId);
|
|
13295
|
-
const attached = on((id) => remote.attached(id));
|
|
13296
|
-
const off = /* @__PURE__ */ new Set();
|
|
13297
|
-
const rules = [
|
|
13298
|
-
["github", github !== void 0],
|
|
13299
|
-
["board", on((id) => assistant.on(id))],
|
|
13300
|
-
["brief", config.briefEnabled],
|
|
13301
|
-
["brief now", on((id) => assistant.on(id))],
|
|
13302
|
-
["agents", remote.enabled],
|
|
13303
|
-
["agent", remote.enabled],
|
|
13304
|
-
["agent detach", attached],
|
|
13305
|
-
["stop it", attached],
|
|
13306
|
-
// ("停掉它" stops the attached agent; with the assistant it is the assistant's to read)
|
|
13307
|
-
["new", !(attached && on((id) => assistant.on(id)))]
|
|
13308
|
-
// (in an attached chat: a new agent session, the assistant's)
|
|
13309
|
-
];
|
|
13310
|
-
for (const [word2, enabled] of rules) if (!enabled) off.add(word2);
|
|
13311
|
-
if (remote.enabled && on((id) => assistant.on(id)) && menuCommand(message.text, group) === "/agent sessions" && !/agent/iu.test(message.text))
|
|
13312
|
-
return "/sessions";
|
|
13313
|
-
return naturalCommand(menuCommand(message.text, group), group, off);
|
|
13314
|
-
}
|
|
13315
|
-
async onText(message, chat, key) {
|
|
13316
|
-
const text8 = this.textOf(message);
|
|
13317
|
-
if (parseCommand(text8) === "unlink" && !message.text.startsWith("/")) {
|
|
13318
|
-
await chat.say(routingText.in(chat.lang()).unlinkAsk);
|
|
13319
|
-
return;
|
|
13320
|
-
}
|
|
13321
|
-
if (await this.commands.run(text8, chat, key, message)) return;
|
|
13322
|
-
if (!this.talksToAgent(message))
|
|
13323
|
-
await this.offerGithub(message, chat, key, text8, (m, fresh) => this.sentOn(m, chat, fresh));
|
|
13324
|
-
await this.sentOn(message, chat, key);
|
|
13325
|
-
}
|
|
13326
|
-
/**
|
|
13327
|
-
* Whether this 1:1 message goes to the remote session the chat is attached to: only words the person typed, and not
|
|
13328
|
-
* those that start by naming the bot. A chat whose session card is the window keeps its chat box for the bot: only a
|
|
13329
|
-
* reply that quotes the card goes to the session.
|
|
13330
|
-
*/
|
|
13331
|
-
talksToAgent(message) {
|
|
13332
|
-
const { remote } = this.ctx.deps;
|
|
13333
|
-
const userId = message.sender.id;
|
|
13334
|
-
if (!remote.attached(userId) || message.provenance !== "typed" || message.namesBotFirst) return false;
|
|
13335
|
-
if (remote.windowMode(userId) === "card") return remote.quotesWindow(userId, message);
|
|
13336
|
-
return true;
|
|
13337
|
-
}
|
|
13338
|
-
/** The files that go to Claw with a message; the person is told about the ones that do not, and they are counted. */
|
|
13339
|
-
async attachments(message, chat, elsewhere = false) {
|
|
13340
|
-
const { messageText, metrics } = this.ctx.deps;
|
|
13341
|
-
const unreadable = messageText.unreadable(message);
|
|
13342
|
-
if (unreadable !== void 0) {
|
|
13343
|
-
metrics.filesDropped("unreadable", unreadable.count);
|
|
13344
|
-
await chat.say(unreadable.line);
|
|
13345
|
-
}
|
|
13346
|
-
if (message.files.length > 0 && elsewhere) {
|
|
13347
|
-
metrics.filesDropped("remote_agent", message.files.length);
|
|
13348
|
-
await chat.say(ELSEWHERE);
|
|
13349
|
-
return [];
|
|
13350
|
-
}
|
|
13351
|
-
return message.files;
|
|
13352
|
-
}
|
|
13353
|
-
/** What the chat is attached to, for the assistant (only where remote agents are on). */
|
|
13354
|
-
async attached(userId, key) {
|
|
13355
|
-
const { remote } = this.ctx.deps;
|
|
13356
|
-
return remote.enabled ? { attached: await remote.chatContext(userId, key) } : {};
|
|
13357
|
-
}
|
|
13358
|
-
/** A 1:1 message that is no command, as it goes on to the attached agent, the assistant or Claw. */
|
|
13359
|
-
async sentOn(message, chat, key) {
|
|
13360
|
-
const { messageText, assistant } = this.ctx.deps;
|
|
13361
|
-
const userId = chat.sender.id;
|
|
13362
|
-
const text8 = this.textOf(message);
|
|
13363
|
-
const toAgent = this.talksToAgent(message);
|
|
13364
|
-
const quote = toAgent && this.ctx.deps.remote.quotesWindow(userId, message) ? "" : messageText.quoteBlock(message, false);
|
|
13365
|
-
const sent = messageText.withLinks(text8, message);
|
|
13366
|
-
const prompt = messageText.withQuote(sent, quote);
|
|
13367
|
-
const files = await this.attachments(message, chat, toAgent);
|
|
13368
|
-
const control = this.sessionControl(userId, prompt, toAgent, quote, files, text8);
|
|
13369
|
-
if (control && assistant.understands(userId)) {
|
|
13370
|
-
await this.understand(message, chat, key);
|
|
13371
|
-
} else if (prompt && toAgent && !control) {
|
|
13372
|
-
await this.remotePrompt(message, chat, key, prompt);
|
|
13373
|
-
} else if (prompt || files.length > 0) {
|
|
13374
|
-
await this.toAssistantOrTurn(message, chat, key, { text: text8, quote, sent, prompt, files });
|
|
13375
|
-
}
|
|
13389
|
+
// packages/core/src/approvals/questions.ts
|
|
13390
|
+
import { z as z4 } from "zod";
|
|
13391
|
+
|
|
13392
|
+
// packages/core/src/text/plain.ts
|
|
13393
|
+
function clip3(text8, limit) {
|
|
13394
|
+
return clipLine(squashSpaces(text8), limit);
|
|
13395
|
+
}
|
|
13396
|
+
function pyJson(value, options = {}) {
|
|
13397
|
+
return options.compact ? compact(value) : pyJsonDumps(value);
|
|
13398
|
+
}
|
|
13399
|
+
function compact(value) {
|
|
13400
|
+
if (Array.isArray(value)) return `[${value.map(compact).join(",")}]`;
|
|
13401
|
+
if (value !== null && typeof value === "object") {
|
|
13402
|
+
const entries = Object.entries(value);
|
|
13403
|
+
return `{${entries.map(([k, v]) => `${pyJsonDumps(k)}:${compact(v)}`).join(",")}}`;
|
|
13376
13404
|
}
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13405
|
+
return pyJsonDumps(value);
|
|
13406
|
+
}
|
|
13407
|
+
|
|
13408
|
+
// packages/core/src/approvals/questions.ts
|
|
13409
|
+
var QUESTION_TOOL2 = "ask_user_question";
|
|
13410
|
+
var QUESTIONS_MAX = 5;
|
|
13411
|
+
var OPTIONS_MAX = 10;
|
|
13412
|
+
var QUESTION_MAX = 500;
|
|
13413
|
+
var OPTION_MAX = 120;
|
|
13414
|
+
var ANSWER_MAX = 1e3;
|
|
13415
|
+
function scalarText(value) {
|
|
13416
|
+
if (value === null || value === void 0 || value === false || value === 0 || value === "") return "";
|
|
13417
|
+
if (typeof value === "string") return value;
|
|
13418
|
+
if (typeof value === "number") return String(value);
|
|
13419
|
+
if (value === true) return "True";
|
|
13420
|
+
return "";
|
|
13421
|
+
}
|
|
13422
|
+
var rawOption = z4.object({ id: z4.unknown(), label: z4.unknown() }).partial();
|
|
13423
|
+
var rawQuestion = z4.object({ id: z4.unknown(), question: z4.unknown(), options: z4.unknown(), allow_multiple: z4.unknown() }).partial();
|
|
13424
|
+
function optionsOf(raw) {
|
|
13425
|
+
const out = [];
|
|
13426
|
+
for (const item of Array.isArray(raw) ? raw : []) {
|
|
13427
|
+
const option = rawOption.safeParse(item);
|
|
13428
|
+
if (!option.success || typeof item !== "object" || Array.isArray(item)) continue;
|
|
13429
|
+
const id = scalarText(option.data.id);
|
|
13430
|
+
if (!pyStrip(id)) continue;
|
|
13431
|
+
out.push({ id, label: clip3(scalarText(option.data.label) || id, OPTION_MAX) });
|
|
13384
13432
|
}
|
|
13385
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
const
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
|
|
13395
|
-
|
|
13396
|
-
|
|
13433
|
+
return out.slice(0, OPTIONS_MAX);
|
|
13434
|
+
}
|
|
13435
|
+
function cardQuestions(raw) {
|
|
13436
|
+
const out = [];
|
|
13437
|
+
for (const item of Array.isArray(raw) ? raw : []) {
|
|
13438
|
+
const question = rawQuestion.safeParse(item);
|
|
13439
|
+
if (!question.success || typeof item !== "object" || Array.isArray(item)) continue;
|
|
13440
|
+
const id = scalarText(question.data.id);
|
|
13441
|
+
const text8 = scalarText(question.data.question);
|
|
13442
|
+
if (!pyStrip(id) || !pyStrip(text8)) continue;
|
|
13443
|
+
out.push({
|
|
13444
|
+
id,
|
|
13445
|
+
question: clip3(text8, QUESTION_MAX),
|
|
13446
|
+
options: optionsOf(question.data.options),
|
|
13447
|
+
allowMultiple: question.data.allow_multiple === true
|
|
13397
13448
|
});
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
13401
|
-
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13449
|
+
}
|
|
13450
|
+
return out.slice(0, QUESTIONS_MAX);
|
|
13451
|
+
}
|
|
13452
|
+
function questionWire(question) {
|
|
13453
|
+
return {
|
|
13454
|
+
id: question.id,
|
|
13455
|
+
question: question.question,
|
|
13456
|
+
options: question.options.map((o) => ({ id: o.id, label: o.label })),
|
|
13457
|
+
allow_multiple: question.allowMultiple
|
|
13458
|
+
};
|
|
13459
|
+
}
|
|
13460
|
+
function answersOf(data) {
|
|
13461
|
+
const answers = /* @__PURE__ */ new Map();
|
|
13462
|
+
const skipped = [];
|
|
13463
|
+
cardQuestions(data["questions"]).forEach((q, i) => {
|
|
13464
|
+
const labels = new Map(q.options.map((o) => [o.id, o.label]));
|
|
13465
|
+
const chosen = scalarText(own(data, `choice_${String(i)}`)).split(",").flatMap((c) => {
|
|
13466
|
+
const label = labels.get(c);
|
|
13467
|
+
return label === void 0 ? [] : [label];
|
|
13409
13468
|
});
|
|
13469
|
+
const typed2 = cpHead(pyStrip(scalarText(own(data, `text_${String(i)}`))), ANSWER_MAX);
|
|
13470
|
+
const answer2 = [chosen.join(", "), typed2].filter(Boolean).join(". ");
|
|
13471
|
+
if (answer2) answers.set(q.id, answer2);
|
|
13472
|
+
else skipped.push(q.id);
|
|
13473
|
+
});
|
|
13474
|
+
return { answers, skipped };
|
|
13475
|
+
}
|
|
13476
|
+
function optionNamed(options, token) {
|
|
13477
|
+
const wanted = token.toLowerCase();
|
|
13478
|
+
const byLabel = options.find((o) => o.label.toLowerCase() === wanted || o.id.toLowerCase() === wanted);
|
|
13479
|
+
if (byLabel !== void 0 || !/^\d{1,2}$/.test(token)) return byLabel;
|
|
13480
|
+
return options[Number(token) - 1];
|
|
13481
|
+
}
|
|
13482
|
+
function typedAnswers(questions, typed2) {
|
|
13483
|
+
const reply = cpHead(pyStrip(typed2), ANSWER_MAX);
|
|
13484
|
+
const answers = /* @__PURE__ */ new Map();
|
|
13485
|
+
const only2 = questions.length === 1 ? questions[0] : void 0;
|
|
13486
|
+
if (only2 !== void 0) {
|
|
13487
|
+
const tokens2 = only2.allowMultiple ? reply.split(/\s*[,,、;;]\s*/u).filter(Boolean) : [reply];
|
|
13488
|
+
const named2 = tokens2.flatMap((t4) => optionNamed(only2.options, t4) ?? []);
|
|
13489
|
+
const labels = named2.length > 0 && named2.length === tokens2.length ? named2.map((o) => o.label) : [];
|
|
13490
|
+
answers.set(only2.id, labels.length ? labels.join(", ") : reply);
|
|
13491
|
+
} else {
|
|
13492
|
+
for (const q of questions) answers.set(q.id, reply);
|
|
13410
13493
|
}
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
13417
|
-
|
|
13418
|
-
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13426
|
-
|
|
13427
|
-
|
|
13428
|
-
|
|
13429
|
-
|
|
13494
|
+
return { answers, skipped: [] };
|
|
13495
|
+
}
|
|
13496
|
+
|
|
13497
|
+
// packages/core/src/approvals/click.ts
|
|
13498
|
+
function viewMessage2(view7, fallbackText = "") {
|
|
13499
|
+
return { body: { type: "view", view: view7, fallbackText } };
|
|
13500
|
+
}
|
|
13501
|
+
|
|
13502
|
+
// packages/core/src/proactive/quiet.ts
|
|
13503
|
+
var APPROVAL_DEFAULT_SECONDS = 600;
|
|
13504
|
+
var PERMISSION_DEFAULT_SECONDS = 600;
|
|
13505
|
+
var PERMISSION_MAX_SECONDS = 3600;
|
|
13506
|
+
var formatters = /* @__PURE__ */ new Map();
|
|
13507
|
+
function formatter(zone) {
|
|
13508
|
+
const known = formatters.get(zone);
|
|
13509
|
+
if (known) return known;
|
|
13510
|
+
try {
|
|
13511
|
+
const made = new Intl.DateTimeFormat("en-US", {
|
|
13512
|
+
timeZone: zone,
|
|
13513
|
+
hour: "2-digit",
|
|
13514
|
+
minute: "2-digit",
|
|
13515
|
+
hourCycle: "h23"
|
|
13516
|
+
});
|
|
13517
|
+
formatters.set(zone, made);
|
|
13518
|
+
return made;
|
|
13519
|
+
} catch {
|
|
13520
|
+
return void 0;
|
|
13521
|
+
}
|
|
13522
|
+
}
|
|
13523
|
+
function zoneOf3(prefs) {
|
|
13524
|
+
return prefs.timezone && formatter(prefs.timezone) ? prefs.timezone : DEFAULT_TIMEZONE;
|
|
13525
|
+
}
|
|
13526
|
+
function localMinutes(epochSeconds2, zone) {
|
|
13527
|
+
const parts2 = (formatter(zone) ?? formatter(DEFAULT_TIMEZONE))?.formatToParts(new Date(epochSeconds2 * 1e3)) ?? [];
|
|
13528
|
+
const value = (type) => Number(parts2.find((p) => p.type === type)?.value ?? "0");
|
|
13529
|
+
return value("hour") * 60 + value("minute");
|
|
13530
|
+
}
|
|
13531
|
+
function minutesOf(clock2) {
|
|
13532
|
+
const [hour = "0", minute = "0"] = clock2.split(":");
|
|
13533
|
+
return Number(hour) * 60 + Number(minute);
|
|
13534
|
+
}
|
|
13535
|
+
function inQuiet(now, zone, start, end) {
|
|
13536
|
+
const minute = localMinutes(now, zone);
|
|
13537
|
+
const s = minutesOf(start);
|
|
13538
|
+
const e = minutesOf(end);
|
|
13539
|
+
if (s === e) return false;
|
|
13540
|
+
return s < e ? s <= minute && minute < e : minute >= s || minute < e;
|
|
13541
|
+
}
|
|
13542
|
+
function muted(prefs, now) {
|
|
13543
|
+
if (prefs.pausedUntil !== null && now < prefs.pausedUntil) return "paused";
|
|
13544
|
+
if (prefs.quietEnabled && inQuiet(now, zoneOf3(prefs), prefs.quietStart, prefs.quietEnd)) return "quiet";
|
|
13545
|
+
return void 0;
|
|
13546
|
+
}
|
|
13547
|
+
function isoSeconds2(raw) {
|
|
13548
|
+
if (typeof raw !== "string") return void 0;
|
|
13549
|
+
const match = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?(Z|[+-]\d{2}:?\d{2}(?::?\d{2})?)$/.exec(
|
|
13550
|
+
raw.trim()
|
|
13551
|
+
);
|
|
13552
|
+
if (!match) return void 0;
|
|
13553
|
+
const at = Date.parse(raw.trim().replace(" ", "T"));
|
|
13554
|
+
return Number.isNaN(at) ? void 0 : at / 1e3;
|
|
13555
|
+
}
|
|
13556
|
+
function approvalExpiry(event, now) {
|
|
13557
|
+
const approval = event["approval"];
|
|
13558
|
+
const raw = typeof approval === "object" && approval !== null ? approval["expires_at"] : void 0;
|
|
13559
|
+
return isoSeconds2(raw) ?? now + APPROVAL_DEFAULT_SECONDS;
|
|
13560
|
+
}
|
|
13561
|
+
function remoteAskExpiry(ask3, now) {
|
|
13562
|
+
const at = isoSeconds2(ask3["expires_at"]);
|
|
13563
|
+
return at === void 0 ? now + PERMISSION_DEFAULT_SECONDS : Math.min(at, now + PERMISSION_MAX_SECONDS);
|
|
13564
|
+
}
|
|
13565
|
+
|
|
13566
|
+
// packages/core/src/routing/question-reply.ts
|
|
13567
|
+
var TYPED_ANSWER_MARGIN_SECONDS = 15;
|
|
13568
|
+
async function answerWaitingQuestion(ctx, reply) {
|
|
13569
|
+
const { store, claw, logger } = ctx.deps;
|
|
13570
|
+
const { chat } = reply;
|
|
13571
|
+
const waiting = waitingQuestion(ctx, reply);
|
|
13572
|
+
if (waiting === void 0) return false;
|
|
13573
|
+
const { pending, questions } = waiting;
|
|
13574
|
+
const { sessionId, toolUseId } = pending;
|
|
13575
|
+
const key = await stillAsked(ctx, reply, pending);
|
|
13576
|
+
if (key === void 0) return false;
|
|
13577
|
+
if (!store.approvals.closeApproval(sessionId, toolUseId, "sending")) return false;
|
|
13578
|
+
const answers = typedAnswers(questions, reply.text);
|
|
13579
|
+
try {
|
|
13580
|
+
await claw.sessions.answer(key, sessionId, toolUseId, Object.fromEntries(answers.answers), answers.skipped);
|
|
13581
|
+
} catch (error) {
|
|
13582
|
+
if (!isClawOrNetwork(error)) throw error;
|
|
13583
|
+
const stale = error instanceof ClawError && [404, 409, 410].includes(error.status);
|
|
13584
|
+
store.approvals.closeApproval(sessionId, toolUseId, stale ? "expired" : "pending", { onlyIfPending: false });
|
|
13585
|
+
logger.warn("a typed answer to a question card was not taken; the message goes on as a new one", {
|
|
13586
|
+
error: statusOrKind(error)
|
|
13587
|
+
});
|
|
13588
|
+
return false;
|
|
13589
|
+
}
|
|
13590
|
+
store.approvals.closeApproval(sessionId, toolUseId, "decided", { onlyIfPending: false });
|
|
13591
|
+
logger.info("a typed message answered the question card waiting in the chat", {
|
|
13592
|
+
chat: chat.scope ? "group" : "1:1",
|
|
13593
|
+
questions: questions.length,
|
|
13594
|
+
card: pending.cardId ? "edited" : "not editable"
|
|
13595
|
+
});
|
|
13596
|
+
const lang = answerLang(store.prefs.getPrefs(chat.sender.id).lang, chat.locale, questions[0]?.question, reply.text);
|
|
13597
|
+
await settleCard(ctx, chat, pending.cardId, [...answers.answers], lang);
|
|
13598
|
+
return true;
|
|
13599
|
+
}
|
|
13600
|
+
function waitingQuestion(ctx, reply) {
|
|
13601
|
+
const { store } = ctx.deps;
|
|
13602
|
+
const { chat } = reply;
|
|
13603
|
+
if (!pyStrip(reply.text)) return void 0;
|
|
13604
|
+
const sessionId = store.sessions.getSession(chat.sender.id, chat.scope);
|
|
13605
|
+
if (sessionId === null || store.reviewDelegation.isActingSession(sessionId)) return void 0;
|
|
13606
|
+
const pending = store.approvals.pendingQuestion(sessionId, chat.sender.id);
|
|
13607
|
+
if (pending === null || pending.expiresAt - store.now() < TYPED_ANSWER_MARGIN_SECONDS) return void 0;
|
|
13608
|
+
const questions = cardQuestions(pending.questions);
|
|
13609
|
+
return questions.length === 0 ? void 0 : { pending, questions };
|
|
13610
|
+
}
|
|
13611
|
+
async function stillAsked(ctx, reply, pending) {
|
|
13612
|
+
const { store, claw, logger } = ctx.deps;
|
|
13613
|
+
try {
|
|
13614
|
+
const key = await reply.credential();
|
|
13615
|
+
if ((await claw.sessions.turn(key, pending.sessionId)).running) return key;
|
|
13616
|
+
store.approvals.closeApproval(pending.sessionId, pending.toolUseId, "expired");
|
|
13617
|
+
return void 0;
|
|
13618
|
+
} catch (error) {
|
|
13619
|
+
if (!isClawOrNetwork(error)) throw error;
|
|
13620
|
+
logger.warn("a typed answer to a question card was not sent: the session's turn could not be read", {
|
|
13621
|
+
error: statusOrKind(error)
|
|
13622
|
+
});
|
|
13623
|
+
return void 0;
|
|
13624
|
+
}
|
|
13625
|
+
}
|
|
13626
|
+
async function settleCard(ctx, chat, cardId, answers, lang) {
|
|
13627
|
+
const { store, logger } = ctx.deps;
|
|
13628
|
+
const zone = zoneOf3(store.prefs.getPrefs(chat.sender.id));
|
|
13629
|
+
const answer2 = [...new Set(answers.map(([, a]) => a))].join(" / ");
|
|
13630
|
+
const words3 = approvalText.in(lang).questionReplied({ answer: answer2 });
|
|
13631
|
+
try {
|
|
13632
|
+
if (cardId) {
|
|
13633
|
+
const view7 = { name: "questionAnswered", model: { answers, when: store.now(), zone, replied: lang } };
|
|
13634
|
+
await chat.conversation.update(cardId, viewMessage2(view7, words3));
|
|
13635
|
+
} else {
|
|
13636
|
+
await chat.say(words3);
|
|
13637
|
+
}
|
|
13638
|
+
} catch (error) {
|
|
13639
|
+
logger.warn("the question card could not be marked answered", { error: errorName(error) });
|
|
13640
|
+
}
|
|
13641
|
+
}
|
|
13642
|
+
|
|
13643
|
+
// packages/core/src/routing/turn-start.ts
|
|
13644
|
+
var MOBILE_PLATFORMS = ["ios", "ipados", "android"];
|
|
13645
|
+
function isMobile(clientPlatform) {
|
|
13646
|
+
return MOBILE_PLATFORMS.includes(clientPlatform.toLowerCase());
|
|
13647
|
+
}
|
|
13648
|
+
function logTurnStarted(logger, clientPlatform, group, route) {
|
|
13649
|
+
logger.info("turn started from Teams client", {
|
|
13650
|
+
platform: clientPlatform || "unknown",
|
|
13651
|
+
chat: group ? "group" : "1:1",
|
|
13652
|
+
route
|
|
13653
|
+
});
|
|
13654
|
+
}
|
|
13655
|
+
var TurnStarter = class {
|
|
13656
|
+
constructor(ctx) {
|
|
13657
|
+
this.ctx = ctx;
|
|
13658
|
+
}
|
|
13659
|
+
ctx;
|
|
13660
|
+
running = /* @__PURE__ */ new Set();
|
|
13661
|
+
/** The turns that run now (a test waits for them). */
|
|
13662
|
+
settled() {
|
|
13663
|
+
return Promise.all([...this.running]).then(() => void 0);
|
|
13664
|
+
}
|
|
13665
|
+
/** The turn that runs (its end is `done`), or undefined when none was started (the person was told why). */
|
|
13666
|
+
async start(started) {
|
|
13667
|
+
const request = { ...started, ...this.replyLang(started) };
|
|
13668
|
+
const { chat, review } = request;
|
|
13669
|
+
if (await this.answersQuestion(request)) return void 0;
|
|
13670
|
+
const key = review ? reviewTurnKey(chat.sender.id, chat.scope, review.itemId, review.pull) : chatTurnKey(chat.sender.id, chat.scope);
|
|
13671
|
+
const turn = new ActiveTurn(review !== void 0, review?.itemId ?? "");
|
|
13672
|
+
const admitted = review === void 0 && await this.admit(request, key, turn);
|
|
13673
|
+
if (admitted === void 0) return void 0;
|
|
13674
|
+
if (!admitted) {
|
|
13675
|
+
const queued = await this.queueBehind(key, turn, chat);
|
|
13676
|
+
if (queued === "refused") return void 0;
|
|
13677
|
+
if (queued === "waiting") return this.track(this.afterFront(request, key, turn));
|
|
13678
|
+
}
|
|
13679
|
+
return this.launch(request, key, turn);
|
|
13680
|
+
}
|
|
13681
|
+
/** A typed message the waiting question card of the chat's session took as its answer (`question-reply.ts`). */
|
|
13682
|
+
async answersQuestion(request) {
|
|
13683
|
+
if (!request.typed || request.review !== void 0 || (request.files?.length ?? 0) > 0) return false;
|
|
13684
|
+
const provider = this.providerOf(request);
|
|
13685
|
+
return answerWaitingQuestion(this.ctx, {
|
|
13686
|
+
chat: request.chat,
|
|
13687
|
+
text: request.text,
|
|
13688
|
+
credential: provider ?? (() => Promise.resolve(request.credential))
|
|
13689
|
+
});
|
|
13690
|
+
}
|
|
13691
|
+
track(task) {
|
|
13692
|
+
this.running.add(task);
|
|
13693
|
+
void task.finally(() => this.running.delete(task));
|
|
13694
|
+
return { done: task };
|
|
13695
|
+
}
|
|
13696
|
+
/** The turn goes to Claw now; its end is `done`. */
|
|
13697
|
+
async launch(request, key, turn) {
|
|
13698
|
+
turn.launched = true;
|
|
13699
|
+
let run;
|
|
13700
|
+
try {
|
|
13701
|
+
run = await this.runOf(request);
|
|
13702
|
+
} catch (error) {
|
|
13703
|
+
this.ctx.turns.drop(key, turn);
|
|
13704
|
+
throw error;
|
|
13705
|
+
}
|
|
13706
|
+
return this.track(this.guarded(request, run, key, turn));
|
|
13707
|
+
}
|
|
13708
|
+
/**
|
|
13709
|
+
* A queued turn: it goes once every turn in front of it is over, or stopped (Claw took its interrupt: Claw is free for the next
|
|
13710
|
+
* message, whether or not that turn ever reports its end); the caller does not wait for it. Not at all when it was given up
|
|
13711
|
+
* meanwhile (`/stop`, said there).
|
|
13712
|
+
*/
|
|
13713
|
+
async afterFront(request, key, turn) {
|
|
13714
|
+
for (; ; ) {
|
|
13715
|
+
if (turn.over) return;
|
|
13716
|
+
const turns = this.ctx.turns.of(key);
|
|
13717
|
+
const at = turns.indexOf(turn);
|
|
13718
|
+
if (at < 0) return;
|
|
13719
|
+
const blocking = turns.slice(0, at).find((front) => !front.stopped.isSet);
|
|
13720
|
+
if (blocking === void 0) break;
|
|
13721
|
+
await Promise.race([blocking.ended.whenSet(), blocking.stopped.whenSet(), turn.ended.whenSet()]);
|
|
13722
|
+
}
|
|
13723
|
+
let started;
|
|
13724
|
+
try {
|
|
13725
|
+
started = await this.launch(request, key, turn);
|
|
13726
|
+
} catch (error) {
|
|
13727
|
+
const ref = this.ctx.deps.random.hexId().slice(0, 8);
|
|
13728
|
+
this.ctx.deps.logger.error("a queued turn could not start", { ref, error: errorName(error) });
|
|
13729
|
+
await this.tell(request, `Something went wrong on my side (ref ${ref}). Please try again.`);
|
|
13730
|
+
return;
|
|
13731
|
+
}
|
|
13732
|
+
await started.done;
|
|
13733
|
+
}
|
|
13734
|
+
/** True when admitted as the chat's first turn; false when the chat has one (queue behind it); undefined: refused. */
|
|
13735
|
+
async admit(request, key, turn) {
|
|
13736
|
+
if (this.ctx.turns.has(key)) return false;
|
|
13737
|
+
const { chat } = request;
|
|
13738
|
+
return this.ctx.admission.lock.run(chat.sender.id, async () => {
|
|
13739
|
+
if (this.ctx.turns.has(key)) return false;
|
|
13740
|
+
if (await this.ctx.admission.overLimit(chat, request.messageId, "claw_turn", "", request.credential))
|
|
13741
|
+
return void 0;
|
|
13742
|
+
this.ctx.turns.claim(key, turn);
|
|
13743
|
+
return true;
|
|
13744
|
+
});
|
|
13745
|
+
}
|
|
13746
|
+
/**
|
|
13747
|
+
* Registers `turn` behind the chat's running one: "front" when the chat has none after all (it runs now), "waiting" when it
|
|
13748
|
+
* is queued (the person is told its place), "refused" (and told why) when it may not wait.
|
|
13749
|
+
*/
|
|
13750
|
+
async queueBehind(key, turn, chat) {
|
|
13751
|
+
const queued = this.ctx.turns.enqueue(key, turn);
|
|
13752
|
+
const said2 = routingText.in(chat.lang());
|
|
13753
|
+
if (queued === "still_working" || queued === "queue_full") {
|
|
13754
|
+
await chat.say(queued === "still_working" ? said2.stillWorking : said2.queueFull({ n: QUEUED_TURNS_MAX }));
|
|
13755
|
+
return "refused";
|
|
13756
|
+
}
|
|
13757
|
+
if (queued.front === turn) return "front";
|
|
13758
|
+
const stoppedInFront = this.ctx.turns.of(key).every((front) => front === turn || front.stopped.isSet);
|
|
13759
|
+
if (stoppedInFront) return "waiting";
|
|
13760
|
+
try {
|
|
13761
|
+
await chat.say(said2.queuedBehind({ n: queued.waiting }));
|
|
13762
|
+
} catch (error) {
|
|
13763
|
+
this.ctx.turns.drop(key, turn);
|
|
13764
|
+
throw error;
|
|
13765
|
+
}
|
|
13766
|
+
return "waiting";
|
|
13767
|
+
}
|
|
13768
|
+
async sessionOf(request) {
|
|
13769
|
+
const { chat, review } = request;
|
|
13770
|
+
const { store, config } = this.ctx.deps;
|
|
13771
|
+
if (review !== void 0) return review.itemSessionId;
|
|
13772
|
+
const sessionId = store.sessions.getSession(chat.sender.id, chat.scope);
|
|
13773
|
+
if (sessionId === null) return null;
|
|
13774
|
+
if (Boolean(store.sessions.getSessionPlugin(chat.sender.id, chat.scope)) !== Boolean(config.pluginId)) {
|
|
13775
|
+
await chat.say("Starting a new session so I can use the Primus platform tools.");
|
|
13776
|
+
return null;
|
|
13777
|
+
}
|
|
13778
|
+
if (chat.scope && !store.sessions.getSessionShared(chat.sender.id, chat.scope)) return null;
|
|
13779
|
+
return sessionId;
|
|
13780
|
+
}
|
|
13781
|
+
providerOf(request) {
|
|
13782
|
+
const usesExchange = request.review !== void 0 || this.ctx.credentials.onExchange;
|
|
13783
|
+
if (!this.ctx.deps.exchange.enabled || !usesExchange) return void 0;
|
|
13784
|
+
return () => this.ctx.credentials.fresh(request.chat.sender.id, request.credential);
|
|
13785
|
+
}
|
|
13786
|
+
ownThumbs(request) {
|
|
13787
|
+
return isMobile(request.clientPlatform) && request.review === void 0 && this.ctx.deps.config.ownThumbsMobile;
|
|
13788
|
+
}
|
|
13789
|
+
/** What the run reads of the chat's session: its id, the model it has, the repository a new one is bound to. */
|
|
13790
|
+
async sessionPart(request) {
|
|
13791
|
+
const { chat, review } = request;
|
|
13792
|
+
const { store } = this.ctx.deps;
|
|
13793
|
+
const sessionId = await this.sessionOf(request);
|
|
13794
|
+
const chosen = sessionId === null && !chat.scope ? store.repoHistory.getPendingRepo(chat.sender.id) : null;
|
|
13795
|
+
return {
|
|
13796
|
+
sessionId,
|
|
13797
|
+
sessionModel: sessionId === null || review ? "" : store.sessions.getSessionModel(chat.sender.id, chat.scope),
|
|
13798
|
+
repo: chosen === null ? null : { url: `${REPO_HOST}/${chosen.repo}`, branch: chosen.branch }
|
|
13799
|
+
};
|
|
13800
|
+
}
|
|
13801
|
+
async runOf(request) {
|
|
13802
|
+
const { chat, review } = request;
|
|
13803
|
+
const { store, config, logger } = this.ctx.deps;
|
|
13804
|
+
const session = await this.sessionPart(request);
|
|
13805
|
+
const group = chat.scope !== "";
|
|
13806
|
+
logTurnStarted(logger, request.clientPlatform, group, review ? "review" : "legacy");
|
|
13807
|
+
return {
|
|
13808
|
+
...session,
|
|
13809
|
+
conversation: chat.conversation,
|
|
13810
|
+
credential: request.credential,
|
|
13811
|
+
provider: this.providerOf(request),
|
|
13812
|
+
text: request.text,
|
|
13813
|
+
messageId: request.messageId,
|
|
13814
|
+
group,
|
|
13815
|
+
userId: chat.sender.id,
|
|
13816
|
+
ownerName: chat.sender.name,
|
|
13817
|
+
model: review ? review.model || config.reviewModel || config.model : config.model,
|
|
13818
|
+
fallbackModel: config.fallbackModel,
|
|
13819
|
+
showChanges: !group && review === void 0,
|
|
13820
|
+
quote: request.quote ?? "",
|
|
13821
|
+
sent: request.sent ?? "",
|
|
13822
|
+
files: request.files ?? [],
|
|
13823
|
+
lang: review?.lang ?? chat.lang(),
|
|
13824
|
+
...request.replyLang === void 0 ? {} : { replyLang: request.replyLang },
|
|
13825
|
+
ownThumbs: this.ownThumbs(request),
|
|
13826
|
+
notifies: this.ctx.deps.subscriptions !== void 0 && store.notifySubs.getNotifySub(chat.sender.id) !== null,
|
|
13827
|
+
review: review?.fields
|
|
13828
|
+
};
|
|
13829
|
+
}
|
|
13830
|
+
/** The language the sender chose to be answered in, for a chat turn (a review's answer follows its request's language). */
|
|
13831
|
+
replyLang(request) {
|
|
13832
|
+
if (request.review !== void 0) return {};
|
|
13833
|
+
const lang = chosenLang(this.ctx.deps.store.prefs.getPrefs(request.chat.sender.id));
|
|
13834
|
+
return lang === void 0 ? {} : { replyLang: lang };
|
|
13835
|
+
}
|
|
13836
|
+
hooks(request, run, turn, turnId) {
|
|
13837
|
+
const { review, chat } = request;
|
|
13838
|
+
const store = this.ctx.deps.store;
|
|
13839
|
+
return {
|
|
13840
|
+
onSession: (sessionId, sessionModel) => {
|
|
13841
|
+
if (review) review.onSession(sessionId);
|
|
13842
|
+
else this.recordSession(chat, sessionId, run, sessionModel);
|
|
13843
|
+
},
|
|
13844
|
+
onRunning: () => {
|
|
13845
|
+
turn.running.set();
|
|
13846
|
+
},
|
|
13847
|
+
onTurn: (sessionId, what, messageId) => {
|
|
13848
|
+
if (what === "started") store.turns.turnStarted(sessionId, { prompt: run.text, messageId, turnId });
|
|
13849
|
+
else if (what === "delivered") store.turns.turnDelivered(sessionId, { messageId, turnId });
|
|
13850
|
+
else store.turns.turnNotDelivered(sessionId, turnId);
|
|
13851
|
+
},
|
|
13852
|
+
stopRequested: () => turn.stopRequested,
|
|
13853
|
+
toolDenied: () => turn.toolDenied,
|
|
13854
|
+
stopConfirmed: () => turn.stopConfirmed,
|
|
13855
|
+
reportView: () => this.ctx.deps.reportView(chat.sender.id, chat.scope)
|
|
13856
|
+
};
|
|
13857
|
+
}
|
|
13858
|
+
async guarded(request, run, key, turn) {
|
|
13859
|
+
const turnId = this.ctx.deps.random.hexId();
|
|
13860
|
+
try {
|
|
13861
|
+
const outcome = await this.ctx.deps.turns.run(run, this.hooks(request, run, turn, turnId));
|
|
13862
|
+
if (request.review) await request.review.after(outcome);
|
|
13863
|
+
} catch (error) {
|
|
13864
|
+
const ref = this.ctx.deps.random.hexId().slice(0, 8);
|
|
13865
|
+
this.ctx.deps.logger.error("turn failed", { ref, error: errorName(error) });
|
|
13866
|
+
await this.tell(request, `Something went wrong on my side (ref ${ref}). Please try again.`);
|
|
13867
|
+
} finally {
|
|
13868
|
+
this.ctx.turns.drop(key, turn);
|
|
13869
|
+
}
|
|
13870
|
+
}
|
|
13871
|
+
async tell(request, text8) {
|
|
13872
|
+
try {
|
|
13873
|
+
await request.chat.say(text8);
|
|
13874
|
+
const report = this.ctx.deps.reportView(request.chat.sender.id, request.chat.scope);
|
|
13875
|
+
if (report !== void 0) await request.chat.send(report);
|
|
13876
|
+
} catch (error) {
|
|
13877
|
+
this.ctx.deps.logger.warn("a failed turn could not be said", { error: errorName(error) });
|
|
13878
|
+
}
|
|
13879
|
+
}
|
|
13880
|
+
/**
|
|
13881
|
+
* The turn has a session: it is the chat's session, and a 1:1 chat remembers it for /sessions, with what a session
|
|
13882
|
+
* this turn created was started with and the repository it is bound to (which uses up the `/repo` choice).
|
|
13883
|
+
*/
|
|
13884
|
+
recordSession(chat, sessionId, run, sessionModel) {
|
|
13885
|
+
const { store, config } = this.ctx.deps;
|
|
13886
|
+
const userId = chat.sender.id;
|
|
13887
|
+
store.sessions.setSession(
|
|
13888
|
+
userId,
|
|
13889
|
+
{ sessionId, pluginId: config.pluginId, model: sessionModel, shared: chat.scope !== "" },
|
|
13890
|
+
chat.scope
|
|
13891
|
+
);
|
|
13892
|
+
if (chat.scope) return;
|
|
13893
|
+
const created = run.sessionId !== sessionId;
|
|
13894
|
+
if (created && run.sessionId) store.repoHistory.forgetSession(userId, run.sessionId);
|
|
13895
|
+
const url = run.repo?.url ?? "";
|
|
13896
|
+
const bound = created && run.repo ? url.startsWith(`${REPO_HOST}/`) ? url.slice(REPO_HOST.length + 1) : url : "";
|
|
13897
|
+
store.repoHistory.noteSession(userId, sessionId, {
|
|
13898
|
+
title: created ? cpHead(squashSpaces(run.text), 50) : "",
|
|
13899
|
+
repo: bound,
|
|
13900
|
+
branch: bound && run.repo ? run.repo.branch : ""
|
|
13901
|
+
});
|
|
13902
|
+
if (created && run.repo) store.repoHistory.clearPendingRepo(userId);
|
|
13903
|
+
}
|
|
13904
|
+
};
|
|
13905
|
+
|
|
13906
|
+
// packages/core/src/routing/personal-text.ts
|
|
13907
|
+
var ELSEWHERE = "Files are not passed on from this chat while it talks to an agent; only your words were sent.";
|
|
13908
|
+
var PersonalText = class {
|
|
13909
|
+
constructor(ctx, commands, starter) {
|
|
13910
|
+
this.ctx = ctx;
|
|
13911
|
+
this.commands = commands;
|
|
13912
|
+
this.starter = starter;
|
|
13913
|
+
}
|
|
13914
|
+
ctx;
|
|
13915
|
+
commands;
|
|
13916
|
+
starter;
|
|
13917
|
+
/**
|
|
13918
|
+
* What the person wrote, with a word of the commands menu read as its command and a sentence help teaches read as the
|
|
13919
|
+
* command it means; a sentence for what this chat cannot do right now stays words. A choice picked on the assistant's card
|
|
13920
|
+
* is the words picked, never a command.
|
|
13921
|
+
*/
|
|
13922
|
+
textOf(message, group = false) {
|
|
13923
|
+
if (message.choice === true) return message.text;
|
|
13924
|
+
const { github, assistant, remote, config } = this.ctx.deps;
|
|
13925
|
+
const userId = group ? void 0 : message.sender.id;
|
|
13926
|
+
const on = (test) => userId !== void 0 && test(userId);
|
|
13927
|
+
const attached = on((id) => remote.attached(id));
|
|
13928
|
+
const off = /* @__PURE__ */ new Set();
|
|
13929
|
+
const rules = [
|
|
13930
|
+
["github", github !== void 0],
|
|
13931
|
+
["board", on((id) => assistant.on(id))],
|
|
13932
|
+
["brief", config.briefEnabled],
|
|
13933
|
+
["brief now", on((id) => assistant.on(id))],
|
|
13934
|
+
["agents", remote.enabled],
|
|
13935
|
+
["agent", remote.enabled],
|
|
13936
|
+
["agent detach", attached],
|
|
13937
|
+
["stop it", attached],
|
|
13938
|
+
// ("停掉它" stops the attached agent; with the assistant it is the assistant's to read)
|
|
13939
|
+
["new", !(attached && on((id) => assistant.on(id)))]
|
|
13940
|
+
// (in an attached chat: a new agent session, the assistant's)
|
|
13941
|
+
];
|
|
13942
|
+
for (const [word2, enabled] of rules) if (!enabled) off.add(word2);
|
|
13943
|
+
if (remote.enabled && on((id) => assistant.on(id)) && menuCommand(message.text, group) === "/agent sessions" && !/agent/iu.test(message.text))
|
|
13944
|
+
return "/sessions";
|
|
13945
|
+
return naturalCommand(menuCommand(message.text, group), group, off);
|
|
13946
|
+
}
|
|
13947
|
+
async onText(message, chat, key) {
|
|
13948
|
+
const text8 = this.textOf(message);
|
|
13949
|
+
if (message.choice === true) {
|
|
13950
|
+
await this.sentOn(message, chat, key);
|
|
13951
|
+
return;
|
|
13952
|
+
}
|
|
13953
|
+
if (parseCommand(text8) === "unlink" && !message.text.startsWith("/")) {
|
|
13954
|
+
await chat.say(routingText.in(chat.lang()).unlinkAsk);
|
|
13955
|
+
return;
|
|
13956
|
+
}
|
|
13957
|
+
if (await this.commands.run(text8, chat, key, message)) return;
|
|
13958
|
+
if (!this.talksToAgent(message))
|
|
13959
|
+
await this.offerGithub(message, chat, key, text8, (m, fresh) => this.sentOn(m, chat, fresh));
|
|
13960
|
+
await this.sentOn(message, chat, key);
|
|
13961
|
+
}
|
|
13962
|
+
/**
|
|
13963
|
+
* Whether this 1:1 message goes to the remote session the chat is attached to: only words the person typed, and not
|
|
13964
|
+
* those that start by naming the bot. A chat whose session card is the window keeps its chat box for the bot: only a
|
|
13965
|
+
* reply that quotes the card goes to the session.
|
|
13966
|
+
*/
|
|
13967
|
+
talksToAgent(message) {
|
|
13968
|
+
const { remote } = this.ctx.deps;
|
|
13969
|
+
const userId = message.sender.id;
|
|
13970
|
+
if (!remote.attached(userId) || message.provenance !== "typed" || message.namesBotFirst) return false;
|
|
13971
|
+
if (remote.windowMode(userId) === "card") return remote.quotesWindow(userId, message);
|
|
13972
|
+
return true;
|
|
13973
|
+
}
|
|
13974
|
+
/** The files that go to Claw with a message; the person is told about the ones that do not, and they are counted. */
|
|
13975
|
+
async attachments(message, chat, elsewhere = false) {
|
|
13976
|
+
const { messageText, metrics } = this.ctx.deps;
|
|
13977
|
+
const unreadable = messageText.unreadable(message);
|
|
13978
|
+
if (unreadable !== void 0) {
|
|
13979
|
+
metrics.filesDropped("unreadable", unreadable.count);
|
|
13980
|
+
await chat.say(unreadable.line);
|
|
13981
|
+
}
|
|
13982
|
+
if (message.files.length > 0 && elsewhere) {
|
|
13983
|
+
metrics.filesDropped("remote_agent", message.files.length);
|
|
13984
|
+
await chat.say(ELSEWHERE);
|
|
13985
|
+
return [];
|
|
13986
|
+
}
|
|
13987
|
+
return message.files;
|
|
13988
|
+
}
|
|
13989
|
+
/** What the chat is attached to, for the assistant (only where remote agents are on). */
|
|
13990
|
+
async attached(userId, key) {
|
|
13991
|
+
const { remote } = this.ctx.deps;
|
|
13992
|
+
return remote.enabled ? { attached: await remote.chatContext(userId, key) } : {};
|
|
13993
|
+
}
|
|
13994
|
+
/** A 1:1 message that is no command, as it goes on to the attached agent, the assistant or Claw. */
|
|
13995
|
+
async sentOn(message, chat, key) {
|
|
13996
|
+
const { messageText, assistant } = this.ctx.deps;
|
|
13997
|
+
const userId = chat.sender.id;
|
|
13998
|
+
const text8 = this.textOf(message);
|
|
13999
|
+
const toAgent = this.talksToAgent(message);
|
|
14000
|
+
const quote = toAgent && this.ctx.deps.remote.quotesWindow(userId, message) ? "" : messageText.quoteBlock(message, false);
|
|
14001
|
+
const sent = messageText.withLinks(text8, message);
|
|
14002
|
+
const prompt = messageText.withQuote(sent, quote);
|
|
14003
|
+
const files = await this.attachments(message, chat, toAgent);
|
|
14004
|
+
const control = message.choice !== true && this.sessionControl(userId, prompt, toAgent, quote, files, text8);
|
|
14005
|
+
if (control && assistant.understands(userId)) {
|
|
14006
|
+
await this.understand(message, chat, key);
|
|
14007
|
+
} else if (prompt && toAgent && !control) {
|
|
14008
|
+
await this.remotePrompt(message, chat, key, prompt);
|
|
14009
|
+
} else if (prompt || files.length > 0) {
|
|
14010
|
+
await this.toAssistantOrTurn(message, chat, key, { text: text8, quote, sent, prompt, files });
|
|
14011
|
+
}
|
|
14012
|
+
}
|
|
14013
|
+
/**
|
|
14014
|
+
* A message to an attached chat that is about the sessions themselves (switch, open a new one), with the assistant on to
|
|
14015
|
+
* read it: the words alone (a quote or a file goes to the agent with them).
|
|
14016
|
+
*/
|
|
14017
|
+
sessionControl(userId, prompt, toAgent, quote, files, text8) {
|
|
14018
|
+
if (!prompt || !toAgent || quote || files.length > 0) return false;
|
|
14019
|
+
return this.ctx.deps.assistant.on(userId) && sessionControl(text8);
|
|
14020
|
+
}
|
|
14021
|
+
/** A message the assistant takes when it is on, else a turn of Claw's. */
|
|
14022
|
+
async toAssistantOrTurn(message, chat, key, said2) {
|
|
14023
|
+
const { assistant } = this.ctx.deps;
|
|
14024
|
+
const userId = chat.sender.id;
|
|
14025
|
+
if (assistant.on(userId) && await this.ctx.admission.assistantOverLimit(chat, message.id, key)) return;
|
|
14026
|
+
const routed = await assistant.route({
|
|
14027
|
+
userId,
|
|
14028
|
+
credential: key,
|
|
14029
|
+
prompt: said2.prompt || assistant.filesOnly(chat.lang()),
|
|
14030
|
+
conversation: chat.conversation,
|
|
14031
|
+
files: said2.files,
|
|
14032
|
+
...await this.attached(userId, key)
|
|
14033
|
+
});
|
|
14034
|
+
if (routed) {
|
|
14035
|
+
logTurnStarted(this.ctx.deps.logger, message.clientPlatform, false, "assistant");
|
|
14036
|
+
return;
|
|
14037
|
+
}
|
|
14038
|
+
this.ctx.admission.dropLastHandoff(userId);
|
|
14039
|
+
await this.starter.start({
|
|
14040
|
+
chat,
|
|
14041
|
+
credential: key,
|
|
14042
|
+
text: said2.text,
|
|
14043
|
+
messageId: message.id,
|
|
14044
|
+
clientPlatform: message.clientPlatform,
|
|
14045
|
+
quote: said2.quote,
|
|
14046
|
+
sent: said2.sent,
|
|
14047
|
+
files: said2.files,
|
|
14048
|
+
typed: true
|
|
14049
|
+
});
|
|
14050
|
+
}
|
|
14051
|
+
/** A prompt for the attached agent, within the task limit (the slot is held until the remote turn counts as running). */
|
|
14052
|
+
async remotePrompt(message, chat, key, prompt) {
|
|
14053
|
+
const { remote } = this.ctx.deps;
|
|
14054
|
+
const request = {
|
|
14055
|
+
message,
|
|
14056
|
+
chat: chat.sender,
|
|
14057
|
+
credential: key,
|
|
14058
|
+
prompt,
|
|
14059
|
+
conversation: chat.conversation,
|
|
14060
|
+
provider: this.ctx.credentials.provider(chat.sender.id, key)
|
|
14061
|
+
};
|
|
14062
|
+
if (remote.running(chat.sender.id)) {
|
|
14063
|
+
await remote.prompt(request);
|
|
14064
|
+
return;
|
|
14065
|
+
}
|
|
14066
|
+
if (!await this.ctx.admission.admitRemote(chat, message.id, key)) return;
|
|
14067
|
+
try {
|
|
14068
|
+
await remote.prompt(request);
|
|
14069
|
+
} finally {
|
|
13430
14070
|
this.ctx.admission.letGo(chat.sender.id);
|
|
13431
14071
|
}
|
|
13432
14072
|
}
|
|
@@ -13493,6 +14133,7 @@ var PersonalText = class {
|
|
|
13493
14133
|
});
|
|
13494
14134
|
const held = assistant.reachable(userId);
|
|
13495
14135
|
if (held && await this.ctx.admission.assistantOverLimit(chat, message.id, key)) return;
|
|
14136
|
+
if (held) logTurnStarted(logger, message.clientPlatform, false, "assistant");
|
|
13496
14137
|
store.assistantMessages.noteAssistantMessage(
|
|
13497
14138
|
channelMessageId,
|
|
13498
14139
|
userId,
|
|
@@ -13573,7 +14214,8 @@ var GroupRouter = class {
|
|
|
13573
14214
|
async message(message, chat) {
|
|
13574
14215
|
const words3 = this.words(message);
|
|
13575
14216
|
learnLang(this.ctx.deps.store, chat.sender.id, message.text);
|
|
13576
|
-
|
|
14217
|
+
const choice = message.choice === true;
|
|
14218
|
+
if (!choice && await this.groupOwn(message, chat, words3)) return;
|
|
13577
14219
|
const route = {
|
|
13578
14220
|
sender: chat.sender,
|
|
13579
14221
|
conversation: chat.conversation,
|
|
@@ -13582,10 +14224,13 @@ var GroupRouter = class {
|
|
|
13582
14224
|
message,
|
|
13583
14225
|
text: words3.text
|
|
13584
14226
|
};
|
|
13585
|
-
|
|
14227
|
+
if (!choice) {
|
|
14228
|
+
for (const interceptor of this.ctx.deps.handlers.groupInterceptors)
|
|
14229
|
+
if (await interceptor.intercept(route)) return;
|
|
14230
|
+
}
|
|
13586
14231
|
const key = await this.ctx.credentials.credential(chat, false);
|
|
13587
14232
|
if (key === void 0) {
|
|
13588
|
-
const keep = parseCommand(words3.text) === null;
|
|
14233
|
+
const keep = choice || parseCommand(words3.text) === null;
|
|
13589
14234
|
await this.onboarding.member(message, chat, { ...words3, keep, identity: false });
|
|
13590
14235
|
return;
|
|
13591
14236
|
}
|
|
@@ -13628,7 +14273,7 @@ var GroupRouter = class {
|
|
|
13628
14273
|
return !(word2 === "/agent" && remoteOn && (verb === "attach" || verb === "detach"));
|
|
13629
14274
|
}
|
|
13630
14275
|
async withKey(message, chat, key, words3) {
|
|
13631
|
-
const command = parseCommand(words3.text);
|
|
14276
|
+
const command = message.choice === true ? null : parseCommand(words3.text);
|
|
13632
14277
|
if (command === "new") await this.sessions.new(chat, chat.scope);
|
|
13633
14278
|
else if (command === "stop") await this.sessions.stop(chat, key, chat.scope);
|
|
13634
14279
|
else if (command === "whoami") await this.whoami(chat, key);
|
|
@@ -13643,7 +14288,8 @@ var GroupRouter = class {
|
|
|
13643
14288
|
clientPlatform: message.clientPlatform,
|
|
13644
14289
|
quote: this.withHistory(message, words3.quote),
|
|
13645
14290
|
sent: words3.sent,
|
|
13646
|
-
files
|
|
14291
|
+
files,
|
|
14292
|
+
typed: true
|
|
13647
14293
|
});
|
|
13648
14294
|
}
|
|
13649
14295
|
}
|
|
@@ -13807,262 +14453,22 @@ var LinkCommands = class {
|
|
|
13807
14453
|
if (!token.ok && token.error.kind === "not_bound") {
|
|
13808
14454
|
const notYet = linkText.in(chat.lang());
|
|
13809
14455
|
const card = await this.ctx.credentials.bindingCard(chat, notYet.notConfirmedYet);
|
|
13810
|
-
await this.ctx.settle(action, chat, card, { fallback: notYet.notConfirmedLine });
|
|
13811
|
-
return;
|
|
13812
|
-
}
|
|
13813
|
-
if (!token.ok) {
|
|
13814
|
-
logger.warn("token exchange failed", { kind: token.error.kind });
|
|
13815
|
-
const text8 = said2.platformUnreachable;
|
|
13816
|
-
const card = this.ctx.views.note(`${text8}${said2.pressConfirmAgain}`, {
|
|
13817
|
-
buttons: [CONFIRM_BUTTON],
|
|
13818
|
-
title: said2.connectTitle({ bot: this.ctx.deps.config.botName })
|
|
13819
|
-
});
|
|
13820
|
-
await this.ctx.settle(action, chat, card, { fallback: text8 });
|
|
13821
|
-
return;
|
|
13822
|
-
}
|
|
13823
|
-
const pending = store.onboarding.takePendingRequest(chat.sender.id);
|
|
13824
|
-
await this.ctx.settle(action, chat, said2.connected + (pending === null ? said2.getStarted : said2.runningFromGroup));
|
|
13825
|
-
if (pending !== null) await this.resume(chat, pending, true);
|
|
13826
|
-
}
|
|
13827
|
-
};
|
|
13828
|
-
|
|
13829
|
-
// packages/core/src/routing/turn-start.ts
|
|
13830
|
-
var MOBILE_PLATFORMS = ["ios", "ipados", "android"];
|
|
13831
|
-
function isMobile(clientPlatform) {
|
|
13832
|
-
return MOBILE_PLATFORMS.includes(clientPlatform.toLowerCase());
|
|
13833
|
-
}
|
|
13834
|
-
var TurnStarter = class {
|
|
13835
|
-
constructor(ctx) {
|
|
13836
|
-
this.ctx = ctx;
|
|
13837
|
-
}
|
|
13838
|
-
ctx;
|
|
13839
|
-
running = /* @__PURE__ */ new Set();
|
|
13840
|
-
/** The turns that run now (a test waits for them). */
|
|
13841
|
-
settled() {
|
|
13842
|
-
return Promise.all([...this.running]).then(() => void 0);
|
|
13843
|
-
}
|
|
13844
|
-
/** The turn that runs (its end is `done`), or undefined when none was started (the person was told why). */
|
|
13845
|
-
async start(request) {
|
|
13846
|
-
const { chat, review } = request;
|
|
13847
|
-
const key = review ? reviewTurnKey(chat.sender.id, chat.scope, review.itemId, review.pull) : chatTurnKey(chat.sender.id, chat.scope);
|
|
13848
|
-
const turn = new ActiveTurn(review !== void 0, review?.itemId ?? "");
|
|
13849
|
-
const admitted = review === void 0 && await this.admit(request, key, turn);
|
|
13850
|
-
if (admitted === void 0) return void 0;
|
|
13851
|
-
if (!admitted) {
|
|
13852
|
-
const queued = await this.queueBehind(key, turn, chat);
|
|
13853
|
-
if (queued === "refused") return void 0;
|
|
13854
|
-
if (queued === "waiting") return this.track(this.afterFront(request, key, turn));
|
|
13855
|
-
}
|
|
13856
|
-
return this.launch(request, key, turn);
|
|
13857
|
-
}
|
|
13858
|
-
track(task) {
|
|
13859
|
-
this.running.add(task);
|
|
13860
|
-
void task.finally(() => this.running.delete(task));
|
|
13861
|
-
return { done: task };
|
|
13862
|
-
}
|
|
13863
|
-
/** The turn goes to Claw now; its end is `done`. */
|
|
13864
|
-
async launch(request, key, turn) {
|
|
13865
|
-
turn.launched = true;
|
|
13866
|
-
let run;
|
|
13867
|
-
try {
|
|
13868
|
-
run = await this.runOf(request);
|
|
13869
|
-
} catch (error) {
|
|
13870
|
-
this.ctx.turns.drop(key, turn);
|
|
13871
|
-
throw error;
|
|
13872
|
-
}
|
|
13873
|
-
return this.track(this.guarded(request, run, key, turn));
|
|
13874
|
-
}
|
|
13875
|
-
/**
|
|
13876
|
-
* A queued turn: it goes once every turn in front of it is over, or stopped (Claw took its interrupt: Claw is free for the next
|
|
13877
|
-
* message, whether or not that turn ever reports its end); the caller does not wait for it. Not at all when it was given up
|
|
13878
|
-
* meanwhile (`/stop`, said there).
|
|
13879
|
-
*/
|
|
13880
|
-
async afterFront(request, key, turn) {
|
|
13881
|
-
for (; ; ) {
|
|
13882
|
-
if (turn.over) return;
|
|
13883
|
-
const turns = this.ctx.turns.of(key);
|
|
13884
|
-
const at = turns.indexOf(turn);
|
|
13885
|
-
if (at < 0) return;
|
|
13886
|
-
const blocking = turns.slice(0, at).find((front) => !front.stopped.isSet);
|
|
13887
|
-
if (blocking === void 0) break;
|
|
13888
|
-
await Promise.race([blocking.ended.whenSet(), blocking.stopped.whenSet(), turn.ended.whenSet()]);
|
|
13889
|
-
}
|
|
13890
|
-
let started;
|
|
13891
|
-
try {
|
|
13892
|
-
started = await this.launch(request, key, turn);
|
|
13893
|
-
} catch (error) {
|
|
13894
|
-
const ref = this.ctx.deps.random.hexId().slice(0, 8);
|
|
13895
|
-
this.ctx.deps.logger.error("a queued turn could not start", { ref, error: errorName(error) });
|
|
13896
|
-
await this.tell(request, `Something went wrong on my side (ref ${ref}). Please try again.`);
|
|
13897
|
-
return;
|
|
13898
|
-
}
|
|
13899
|
-
await started.done;
|
|
13900
|
-
}
|
|
13901
|
-
/** True when admitted as the chat's first turn; false when the chat has one (queue behind it); undefined: refused. */
|
|
13902
|
-
async admit(request, key, turn) {
|
|
13903
|
-
if (this.ctx.turns.has(key)) return false;
|
|
13904
|
-
const { chat } = request;
|
|
13905
|
-
return this.ctx.admission.lock.run(chat.sender.id, async () => {
|
|
13906
|
-
if (this.ctx.turns.has(key)) return false;
|
|
13907
|
-
if (await this.ctx.admission.overLimit(chat, request.messageId, "claw_turn", "", request.credential))
|
|
13908
|
-
return void 0;
|
|
13909
|
-
this.ctx.turns.claim(key, turn);
|
|
13910
|
-
return true;
|
|
13911
|
-
});
|
|
13912
|
-
}
|
|
13913
|
-
/**
|
|
13914
|
-
* Registers `turn` behind the chat's running one: "front" when the chat has none after all (it runs now), "waiting" when it
|
|
13915
|
-
* is queued (the person is told its place), "refused" (and told why) when it may not wait.
|
|
13916
|
-
*/
|
|
13917
|
-
async queueBehind(key, turn, chat) {
|
|
13918
|
-
const queued = this.ctx.turns.enqueue(key, turn);
|
|
13919
|
-
const said2 = routingText.in(chat.lang());
|
|
13920
|
-
if (queued === "still_working" || queued === "queue_full") {
|
|
13921
|
-
await chat.say(queued === "still_working" ? said2.stillWorking : said2.queueFull({ n: QUEUED_TURNS_MAX }));
|
|
13922
|
-
return "refused";
|
|
13923
|
-
}
|
|
13924
|
-
if (queued.front === turn) return "front";
|
|
13925
|
-
const stoppedInFront = this.ctx.turns.of(key).every((front) => front === turn || front.stopped.isSet);
|
|
13926
|
-
if (stoppedInFront) return "waiting";
|
|
13927
|
-
try {
|
|
13928
|
-
await chat.say(said2.queuedBehind({ n: queued.waiting }));
|
|
13929
|
-
} catch (error) {
|
|
13930
|
-
this.ctx.turns.drop(key, turn);
|
|
13931
|
-
throw error;
|
|
13932
|
-
}
|
|
13933
|
-
return "waiting";
|
|
13934
|
-
}
|
|
13935
|
-
async sessionOf(request) {
|
|
13936
|
-
const { chat, review } = request;
|
|
13937
|
-
const { store, config } = this.ctx.deps;
|
|
13938
|
-
if (review !== void 0) return review.itemSessionId;
|
|
13939
|
-
const sessionId = store.sessions.getSession(chat.sender.id, chat.scope);
|
|
13940
|
-
if (sessionId === null) return null;
|
|
13941
|
-
if (Boolean(store.sessions.getSessionPlugin(chat.sender.id, chat.scope)) !== Boolean(config.pluginId)) {
|
|
13942
|
-
await chat.say("Starting a new session so I can use the Primus platform tools.");
|
|
13943
|
-
return null;
|
|
13944
|
-
}
|
|
13945
|
-
if (chat.scope && !store.sessions.getSessionShared(chat.sender.id, chat.scope)) return null;
|
|
13946
|
-
return sessionId;
|
|
13947
|
-
}
|
|
13948
|
-
providerOf(request) {
|
|
13949
|
-
const usesExchange = request.review !== void 0 || this.ctx.credentials.onExchange;
|
|
13950
|
-
if (!this.ctx.deps.exchange.enabled || !usesExchange) return void 0;
|
|
13951
|
-
return () => this.ctx.credentials.fresh(request.chat.sender.id, request.credential);
|
|
13952
|
-
}
|
|
13953
|
-
ownThumbs(request) {
|
|
13954
|
-
return isMobile(request.clientPlatform) && request.review === void 0 && this.ctx.deps.config.ownThumbsMobile;
|
|
13955
|
-
}
|
|
13956
|
-
/** What the run reads of the chat's session: its id, the model it has, the repository a new one is bound to. */
|
|
13957
|
-
async sessionPart(request) {
|
|
13958
|
-
const { chat, review } = request;
|
|
13959
|
-
const { store } = this.ctx.deps;
|
|
13960
|
-
const sessionId = await this.sessionOf(request);
|
|
13961
|
-
const chosen = sessionId === null && !chat.scope ? store.repoHistory.getPendingRepo(chat.sender.id) : null;
|
|
13962
|
-
return {
|
|
13963
|
-
sessionId,
|
|
13964
|
-
sessionModel: sessionId === null || review ? "" : store.sessions.getSessionModel(chat.sender.id, chat.scope),
|
|
13965
|
-
repo: chosen === null ? null : { url: `${REPO_HOST}/${chosen.repo}`, branch: chosen.branch }
|
|
13966
|
-
};
|
|
13967
|
-
}
|
|
13968
|
-
async runOf(request) {
|
|
13969
|
-
const { chat, review } = request;
|
|
13970
|
-
const { store, config, logger } = this.ctx.deps;
|
|
13971
|
-
const session = await this.sessionPart(request);
|
|
13972
|
-
const group = chat.scope !== "";
|
|
13973
|
-
logger.info("turn started from Teams client", {
|
|
13974
|
-
platform: request.clientPlatform || "unknown",
|
|
13975
|
-
chat: group ? "group" : "1:1"
|
|
13976
|
-
});
|
|
13977
|
-
return {
|
|
13978
|
-
...session,
|
|
13979
|
-
conversation: chat.conversation,
|
|
13980
|
-
credential: request.credential,
|
|
13981
|
-
provider: this.providerOf(request),
|
|
13982
|
-
text: request.text,
|
|
13983
|
-
messageId: request.messageId,
|
|
13984
|
-
group,
|
|
13985
|
-
userId: chat.sender.id,
|
|
13986
|
-
ownerName: chat.sender.name,
|
|
13987
|
-
model: review ? review.model || config.reviewModel || config.model : config.model,
|
|
13988
|
-
fallbackModel: config.fallbackModel,
|
|
13989
|
-
showChanges: !group && review === void 0,
|
|
13990
|
-
quote: request.quote ?? "",
|
|
13991
|
-
sent: request.sent ?? "",
|
|
13992
|
-
files: request.files ?? [],
|
|
13993
|
-
lang: review?.lang ?? chat.lang(),
|
|
13994
|
-
ownThumbs: this.ownThumbs(request),
|
|
13995
|
-
notifies: this.ctx.deps.subscriptions !== void 0 && store.notifySubs.getNotifySub(chat.sender.id) !== null,
|
|
13996
|
-
review: review?.fields
|
|
13997
|
-
};
|
|
13998
|
-
}
|
|
13999
|
-
hooks(request, run, turn, turnId) {
|
|
14000
|
-
const { review, chat } = request;
|
|
14001
|
-
const store = this.ctx.deps.store;
|
|
14002
|
-
return {
|
|
14003
|
-
onSession: (sessionId, sessionModel) => {
|
|
14004
|
-
if (review) review.onSession(sessionId);
|
|
14005
|
-
else this.recordSession(chat, sessionId, run, sessionModel);
|
|
14006
|
-
},
|
|
14007
|
-
onRunning: () => {
|
|
14008
|
-
turn.running.set();
|
|
14009
|
-
},
|
|
14010
|
-
onTurn: (sessionId, what, messageId) => {
|
|
14011
|
-
if (what === "started") store.turns.turnStarted(sessionId, { prompt: run.text, messageId, turnId });
|
|
14012
|
-
else if (what === "delivered") store.turns.turnDelivered(sessionId, { messageId, turnId });
|
|
14013
|
-
else store.turns.turnNotDelivered(sessionId, turnId);
|
|
14014
|
-
},
|
|
14015
|
-
stopRequested: () => turn.stopRequested,
|
|
14016
|
-
toolDenied: () => turn.toolDenied,
|
|
14017
|
-
stopConfirmed: () => turn.stopConfirmed,
|
|
14018
|
-
reportView: () => this.ctx.deps.reportView(chat.sender.id, chat.scope)
|
|
14019
|
-
};
|
|
14020
|
-
}
|
|
14021
|
-
async guarded(request, run, key, turn) {
|
|
14022
|
-
const turnId = this.ctx.deps.random.hexId();
|
|
14023
|
-
try {
|
|
14024
|
-
const outcome = await this.ctx.deps.turns.run(run, this.hooks(request, run, turn, turnId));
|
|
14025
|
-
if (request.review) await request.review.after(outcome);
|
|
14026
|
-
} catch (error) {
|
|
14027
|
-
const ref = this.ctx.deps.random.hexId().slice(0, 8);
|
|
14028
|
-
this.ctx.deps.logger.error("turn failed", { ref, error: errorName(error) });
|
|
14029
|
-
await this.tell(request, `Something went wrong on my side (ref ${ref}). Please try again.`);
|
|
14030
|
-
} finally {
|
|
14031
|
-
this.ctx.turns.drop(key, turn);
|
|
14032
|
-
}
|
|
14033
|
-
}
|
|
14034
|
-
async tell(request, text8) {
|
|
14035
|
-
try {
|
|
14036
|
-
await request.chat.say(text8);
|
|
14037
|
-
const report = this.ctx.deps.reportView(request.chat.sender.id, request.chat.scope);
|
|
14038
|
-
if (report !== void 0) await request.chat.send(report);
|
|
14039
|
-
} catch (error) {
|
|
14040
|
-
this.ctx.deps.logger.warn("a failed turn could not be said", { error: errorName(error) });
|
|
14456
|
+
await this.ctx.settle(action, chat, card, { fallback: notYet.notConfirmedLine });
|
|
14457
|
+
return;
|
|
14041
14458
|
}
|
|
14042
|
-
|
|
14043
|
-
|
|
14044
|
-
|
|
14045
|
-
|
|
14046
|
-
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
|
|
14053
|
-
|
|
14054
|
-
);
|
|
14055
|
-
if (chat.scope) return;
|
|
14056
|
-
const created = run.sessionId !== sessionId;
|
|
14057
|
-
if (created && run.sessionId) store.repoHistory.forgetSession(userId, run.sessionId);
|
|
14058
|
-
const url = run.repo?.url ?? "";
|
|
14059
|
-
const bound = created && run.repo ? url.startsWith(`${REPO_HOST}/`) ? url.slice(REPO_HOST.length + 1) : url : "";
|
|
14060
|
-
store.repoHistory.noteSession(userId, sessionId, {
|
|
14061
|
-
title: created ? cpHead(squashSpaces(run.text), 50) : "",
|
|
14062
|
-
repo: bound,
|
|
14063
|
-
branch: bound && run.repo ? run.repo.branch : ""
|
|
14064
|
-
});
|
|
14065
|
-
if (created && run.repo) store.repoHistory.clearPendingRepo(userId);
|
|
14459
|
+
if (!token.ok) {
|
|
14460
|
+
logger.warn("token exchange failed", { kind: token.error.kind });
|
|
14461
|
+
const text8 = said2.platformUnreachable;
|
|
14462
|
+
const card = this.ctx.views.note(`${text8}${said2.pressConfirmAgain}`, {
|
|
14463
|
+
buttons: [CONFIRM_BUTTON],
|
|
14464
|
+
title: said2.connectTitle({ bot: this.ctx.deps.config.botName })
|
|
14465
|
+
});
|
|
14466
|
+
await this.ctx.settle(action, chat, card, { fallback: text8 });
|
|
14467
|
+
return;
|
|
14468
|
+
}
|
|
14469
|
+
const pending = store.onboarding.takePendingRequest(chat.sender.id);
|
|
14470
|
+
await this.ctx.settle(action, chat, said2.connected + (pending === null ? said2.getStarted : said2.runningFromGroup));
|
|
14471
|
+
if (pending !== null) await this.resume(chat, pending, true);
|
|
14066
14472
|
}
|
|
14067
14473
|
};
|
|
14068
14474
|
|
|
@@ -14171,7 +14577,7 @@ var Router = class _Router {
|
|
|
14171
14577
|
*/
|
|
14172
14578
|
async withinRate(message, chat) {
|
|
14173
14579
|
const group = message.conversation.kind === "group";
|
|
14174
|
-
if (group && !message.mentioned || _Router.isStop(message.text, group)) return true;
|
|
14580
|
+
if (group && !message.mentioned || message.choice !== true && _Router.isStop(message.text, group)) return true;
|
|
14175
14581
|
const { limits, securityEvent, metrics } = this.ctx.deps;
|
|
14176
14582
|
const verdict = limits.checkMessage(chat.sender.id);
|
|
14177
14583
|
if (verdict.allowed) return true;
|
|
@@ -14228,22 +14634,6 @@ var Router = class _Router {
|
|
|
14228
14634
|
}
|
|
14229
14635
|
};
|
|
14230
14636
|
|
|
14231
|
-
// packages/core/src/text/plain.ts
|
|
14232
|
-
function clip3(text8, limit) {
|
|
14233
|
-
return clipLine(squashSpaces(text8), limit);
|
|
14234
|
-
}
|
|
14235
|
-
function pyJson(value, options = {}) {
|
|
14236
|
-
return options.compact ? compact(value) : pyJsonDumps(value);
|
|
14237
|
-
}
|
|
14238
|
-
function compact(value) {
|
|
14239
|
-
if (Array.isArray(value)) return `[${value.map(compact).join(",")}]`;
|
|
14240
|
-
if (value !== null && typeof value === "object") {
|
|
14241
|
-
const entries = Object.entries(value);
|
|
14242
|
-
return `{${entries.map(([k, v]) => `${pyJsonDumps(k)}:${compact(v)}`).join(",")}}`;
|
|
14243
|
-
}
|
|
14244
|
-
return pyJsonDumps(value);
|
|
14245
|
-
}
|
|
14246
|
-
|
|
14247
14637
|
// packages/core/src/approvals/sensitive.ts
|
|
14248
14638
|
var WORDS = /* @__PURE__ */ new Set([
|
|
14249
14639
|
"token",
|
|
@@ -14510,145 +14900,6 @@ function approvalView(request) {
|
|
|
14510
14900
|
};
|
|
14511
14901
|
}
|
|
14512
14902
|
|
|
14513
|
-
// packages/core/src/approvals/click.ts
|
|
14514
|
-
function viewMessage2(view7, fallbackText = "") {
|
|
14515
|
-
return { body: { type: "view", view: view7, fallbackText } };
|
|
14516
|
-
}
|
|
14517
|
-
|
|
14518
|
-
// packages/core/src/proactive/quiet.ts
|
|
14519
|
-
var APPROVAL_DEFAULT_SECONDS = 600;
|
|
14520
|
-
var PERMISSION_DEFAULT_SECONDS = 600;
|
|
14521
|
-
var PERMISSION_MAX_SECONDS = 3600;
|
|
14522
|
-
var formatters = /* @__PURE__ */ new Map();
|
|
14523
|
-
function formatter(zone) {
|
|
14524
|
-
const known = formatters.get(zone);
|
|
14525
|
-
if (known) return known;
|
|
14526
|
-
try {
|
|
14527
|
-
const made = new Intl.DateTimeFormat("en-US", {
|
|
14528
|
-
timeZone: zone,
|
|
14529
|
-
hour: "2-digit",
|
|
14530
|
-
minute: "2-digit",
|
|
14531
|
-
hourCycle: "h23"
|
|
14532
|
-
});
|
|
14533
|
-
formatters.set(zone, made);
|
|
14534
|
-
return made;
|
|
14535
|
-
} catch {
|
|
14536
|
-
return void 0;
|
|
14537
|
-
}
|
|
14538
|
-
}
|
|
14539
|
-
function zoneOf3(prefs) {
|
|
14540
|
-
return prefs.timezone && formatter(prefs.timezone) ? prefs.timezone : DEFAULT_TIMEZONE;
|
|
14541
|
-
}
|
|
14542
|
-
function localMinutes(epochSeconds2, zone) {
|
|
14543
|
-
const parts2 = (formatter(zone) ?? formatter(DEFAULT_TIMEZONE))?.formatToParts(new Date(epochSeconds2 * 1e3)) ?? [];
|
|
14544
|
-
const value = (type) => Number(parts2.find((p) => p.type === type)?.value ?? "0");
|
|
14545
|
-
return value("hour") * 60 + value("minute");
|
|
14546
|
-
}
|
|
14547
|
-
function minutesOf(clock2) {
|
|
14548
|
-
const [hour = "0", minute = "0"] = clock2.split(":");
|
|
14549
|
-
return Number(hour) * 60 + Number(minute);
|
|
14550
|
-
}
|
|
14551
|
-
function inQuiet(now, zone, start, end) {
|
|
14552
|
-
const minute = localMinutes(now, zone);
|
|
14553
|
-
const s = minutesOf(start);
|
|
14554
|
-
const e = minutesOf(end);
|
|
14555
|
-
if (s === e) return false;
|
|
14556
|
-
return s < e ? s <= minute && minute < e : minute >= s || minute < e;
|
|
14557
|
-
}
|
|
14558
|
-
function muted(prefs, now) {
|
|
14559
|
-
if (prefs.pausedUntil !== null && now < prefs.pausedUntil) return "paused";
|
|
14560
|
-
if (prefs.quietEnabled && inQuiet(now, zoneOf3(prefs), prefs.quietStart, prefs.quietEnd)) return "quiet";
|
|
14561
|
-
return void 0;
|
|
14562
|
-
}
|
|
14563
|
-
function isoSeconds2(raw) {
|
|
14564
|
-
if (typeof raw !== "string") return void 0;
|
|
14565
|
-
const match = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?(Z|[+-]\d{2}:?\d{2}(?::?\d{2})?)$/.exec(
|
|
14566
|
-
raw.trim()
|
|
14567
|
-
);
|
|
14568
|
-
if (!match) return void 0;
|
|
14569
|
-
const at = Date.parse(raw.trim().replace(" ", "T"));
|
|
14570
|
-
return Number.isNaN(at) ? void 0 : at / 1e3;
|
|
14571
|
-
}
|
|
14572
|
-
function approvalExpiry(event, now) {
|
|
14573
|
-
const approval = event["approval"];
|
|
14574
|
-
const raw = typeof approval === "object" && approval !== null ? approval["expires_at"] : void 0;
|
|
14575
|
-
return isoSeconds2(raw) ?? now + APPROVAL_DEFAULT_SECONDS;
|
|
14576
|
-
}
|
|
14577
|
-
function remoteAskExpiry(ask3, now) {
|
|
14578
|
-
const at = isoSeconds2(ask3["expires_at"]);
|
|
14579
|
-
return at === void 0 ? now + PERMISSION_DEFAULT_SECONDS : Math.min(at, now + PERMISSION_MAX_SECONDS);
|
|
14580
|
-
}
|
|
14581
|
-
|
|
14582
|
-
// packages/core/src/approvals/questions.ts
|
|
14583
|
-
import { z as z4 } from "zod";
|
|
14584
|
-
var QUESTION_TOOL2 = "ask_user_question";
|
|
14585
|
-
var QUESTIONS_MAX = 5;
|
|
14586
|
-
var OPTIONS_MAX = 10;
|
|
14587
|
-
var QUESTION_MAX = 500;
|
|
14588
|
-
var OPTION_MAX = 120;
|
|
14589
|
-
var ANSWER_MAX = 1e3;
|
|
14590
|
-
function scalarText(value) {
|
|
14591
|
-
if (value === null || value === void 0 || value === false || value === 0 || value === "") return "";
|
|
14592
|
-
if (typeof value === "string") return value;
|
|
14593
|
-
if (typeof value === "number") return String(value);
|
|
14594
|
-
if (value === true) return "True";
|
|
14595
|
-
return "";
|
|
14596
|
-
}
|
|
14597
|
-
var rawOption = z4.object({ id: z4.unknown(), label: z4.unknown() }).partial();
|
|
14598
|
-
var rawQuestion = z4.object({ id: z4.unknown(), question: z4.unknown(), options: z4.unknown(), allow_multiple: z4.unknown() }).partial();
|
|
14599
|
-
function optionsOf(raw) {
|
|
14600
|
-
const out = [];
|
|
14601
|
-
for (const item of Array.isArray(raw) ? raw : []) {
|
|
14602
|
-
const option = rawOption.safeParse(item);
|
|
14603
|
-
if (!option.success || typeof item !== "object" || Array.isArray(item)) continue;
|
|
14604
|
-
const id = scalarText(option.data.id);
|
|
14605
|
-
if (!pyStrip(id)) continue;
|
|
14606
|
-
out.push({ id, label: clip3(scalarText(option.data.label) || id, OPTION_MAX) });
|
|
14607
|
-
}
|
|
14608
|
-
return out.slice(0, OPTIONS_MAX);
|
|
14609
|
-
}
|
|
14610
|
-
function cardQuestions(raw) {
|
|
14611
|
-
const out = [];
|
|
14612
|
-
for (const item of Array.isArray(raw) ? raw : []) {
|
|
14613
|
-
const question = rawQuestion.safeParse(item);
|
|
14614
|
-
if (!question.success || typeof item !== "object" || Array.isArray(item)) continue;
|
|
14615
|
-
const id = scalarText(question.data.id);
|
|
14616
|
-
const text8 = scalarText(question.data.question);
|
|
14617
|
-
if (!pyStrip(id) || !pyStrip(text8)) continue;
|
|
14618
|
-
out.push({
|
|
14619
|
-
id,
|
|
14620
|
-
question: clip3(text8, QUESTION_MAX),
|
|
14621
|
-
options: optionsOf(question.data.options),
|
|
14622
|
-
allowMultiple: question.data.allow_multiple === true
|
|
14623
|
-
});
|
|
14624
|
-
}
|
|
14625
|
-
return out.slice(0, QUESTIONS_MAX);
|
|
14626
|
-
}
|
|
14627
|
-
function questionWire(question) {
|
|
14628
|
-
return {
|
|
14629
|
-
id: question.id,
|
|
14630
|
-
question: question.question,
|
|
14631
|
-
options: question.options.map((o) => ({ id: o.id, label: o.label })),
|
|
14632
|
-
allow_multiple: question.allowMultiple
|
|
14633
|
-
};
|
|
14634
|
-
}
|
|
14635
|
-
function answersOf(data) {
|
|
14636
|
-
const answers = /* @__PURE__ */ new Map();
|
|
14637
|
-
const skipped = [];
|
|
14638
|
-
cardQuestions(data["questions"]).forEach((q, i) => {
|
|
14639
|
-
const labels = new Map(q.options.map((o) => [o.id, o.label]));
|
|
14640
|
-
const chosen = scalarText(own(data, `choice_${String(i)}`)).split(",").flatMap((c) => {
|
|
14641
|
-
const label = labels.get(c);
|
|
14642
|
-
return label === void 0 ? [] : [label];
|
|
14643
|
-
});
|
|
14644
|
-
const typed2 = cpHead(pyStrip(scalarText(own(data, `text_${String(i)}`))), ANSWER_MAX);
|
|
14645
|
-
const answer2 = [chosen.join(", "), typed2].filter(Boolean).join(". ");
|
|
14646
|
-
if (answer2) answers.set(q.id, answer2);
|
|
14647
|
-
else skipped.push(q.id);
|
|
14648
|
-
});
|
|
14649
|
-
return { answers, skipped };
|
|
14650
|
-
}
|
|
14651
|
-
|
|
14652
14903
|
// packages/core/src/approvals/decide.ts
|
|
14653
14904
|
function str(value) {
|
|
14654
14905
|
return typeof value === "string" ? value : typeof value === "number" ? String(value) : "";
|
|
@@ -14951,16 +15202,18 @@ var ApprovalCards = class {
|
|
|
14951
15202
|
var TURN_DEADLINE_SECONDS2 = 30 * 60;
|
|
14952
15203
|
function turnApprovals(store) {
|
|
14953
15204
|
return {
|
|
14954
|
-
record: (userId, sessionId, toolUseId, tool2, expiresIn, envelope) => store.approvals.saveApproval({
|
|
15205
|
+
record: (userId, sessionId, toolUseId, tool2, expiresIn, envelope, questions) => store.approvals.saveApproval({
|
|
14955
15206
|
userId,
|
|
14956
15207
|
sessionId,
|
|
14957
15208
|
toolUseId,
|
|
14958
15209
|
tool: tool2,
|
|
14959
15210
|
expiresAt: store.now() + (expiresIn || TURN_DEADLINE_SECONDS2),
|
|
14960
|
-
envelope
|
|
15211
|
+
envelope,
|
|
15212
|
+
// (a question's, as its card shows them: what a typed answer is matched against)
|
|
15213
|
+
...questions === void 0 ? {} : { questions: cardQuestions(questions).map((q) => questionWire(q)) }
|
|
14961
15214
|
}),
|
|
14962
|
-
sent: (sessionId, toolUseId) => {
|
|
14963
|
-
store.approvals.approvalSent(sessionId, toolUseId);
|
|
15215
|
+
sent: (sessionId, toolUseId, cardId) => {
|
|
15216
|
+
store.approvals.approvalSent(sessionId, toolUseId, cardId);
|
|
14964
15217
|
},
|
|
14965
15218
|
unsent: (sessionId, toolUseId) => {
|
|
14966
15219
|
store.approvals.forgetApproval(sessionId, toolUseId);
|
|
@@ -14982,7 +15235,15 @@ var approvalTurnCards = {
|
|
|
14982
15235
|
return viewMessage2(
|
|
14983
15236
|
{
|
|
14984
15237
|
name: "question",
|
|
14985
|
-
model: {
|
|
15238
|
+
model: {
|
|
15239
|
+
questions: cardQuestions(ask3.questions),
|
|
15240
|
+
sessionId,
|
|
15241
|
+
toolUseId,
|
|
15242
|
+
group,
|
|
15243
|
+
ownerName,
|
|
15244
|
+
note: "",
|
|
15245
|
+
typedReply: true
|
|
15246
|
+
}
|
|
14986
15247
|
},
|
|
14987
15248
|
QUESTION_TOOL2
|
|
14988
15249
|
);
|
|
@@ -15338,6 +15599,10 @@ var AssistantChat = class {
|
|
|
15338
15599
|
filesOnly(lang) {
|
|
15339
15600
|
return assistantChatText.in(lang).filesOnly;
|
|
15340
15601
|
}
|
|
15602
|
+
/** The language the person chose to be answered in, as the message's `channel_context` carries it (none: nothing is sent). */
|
|
15603
|
+
chosenLanguage(userId) {
|
|
15604
|
+
return replyLanguageContext(this.o.store.prefs.getPrefs(userId));
|
|
15605
|
+
}
|
|
15341
15606
|
langOf(userId, text8) {
|
|
15342
15607
|
return answerLang(this.o.store.prefs.getPrefs(userId).lang, this.o.clientLocale?.() ?? "", text8);
|
|
15343
15608
|
}
|
|
@@ -15357,10 +15622,11 @@ var AssistantChat = class {
|
|
|
15357
15622
|
const handing = { ...request };
|
|
15358
15623
|
const sent = await this.handTo(handing, async (sessionId, ready) => {
|
|
15359
15624
|
const folderId = typeof request.context["channel_message_id"] === "string" ? request.context["channel_message_id"] : "";
|
|
15360
|
-
const context2 =
|
|
15625
|
+
const context2 = {
|
|
15361
15626
|
...request.context,
|
|
15362
|
-
files: await this.handOverFiles(handing, sessionId, folderId || this.o.random.hexId(), request.files)
|
|
15363
|
-
|
|
15627
|
+
...request.files.length ? { files: await this.handOverFiles(handing, sessionId, folderId || this.o.random.hexId(), request.files) } : {},
|
|
15628
|
+
...this.chosenLanguage(request.userId)
|
|
15629
|
+
};
|
|
15364
15630
|
await ready();
|
|
15365
15631
|
return this.o.claw.assistant.send(request.credential, sessionId, request.prompt, {
|
|
15366
15632
|
...request.modelKey ? { modelKey: request.modelKey } : {},
|
|
@@ -15432,7 +15698,7 @@ var AssistantChat = class {
|
|
|
15432
15698
|
async (_sessionId, ready) => {
|
|
15433
15699
|
const files = request.files.length ? { files: await this.handOverFiles(handing, sessionId, this.o.random.hexId(), request.files) } : {};
|
|
15434
15700
|
const attached = request.attached === void 0 ? {} : { attached: attachedOf(request.attached) };
|
|
15435
|
-
const context2 = { ...files, ...attached };
|
|
15701
|
+
const context2 = { ...files, ...attached, ...this.chosenLanguage(request.userId) };
|
|
15436
15702
|
await ready();
|
|
15437
15703
|
return this.o.claw.assistant.send(request.credential, sessionId, request.prompt, {
|
|
15438
15704
|
...Object.keys(context2).length ? { context: context2 } : {},
|
|
@@ -15933,6 +16199,117 @@ var AssistantActions = class {
|
|
|
15933
16199
|
}
|
|
15934
16200
|
};
|
|
15935
16201
|
|
|
16202
|
+
// packages/core/src/assistant/choice-clicks.ts
|
|
16203
|
+
var CHOICE_VERB = "assistant_choice";
|
|
16204
|
+
var CHOICE_INPUT = "choice";
|
|
16205
|
+
function pickedOn(card, value) {
|
|
16206
|
+
const places = new Set(
|
|
16207
|
+
pyStr(value).split(",").map((p) => p.trim()).filter((p) => /^\d{1,2}$/.test(p)).map(Number)
|
|
16208
|
+
);
|
|
16209
|
+
return card.options.filter((_, i) => places.has(i));
|
|
16210
|
+
}
|
|
16211
|
+
function typedMessage(click, text8, id) {
|
|
16212
|
+
const { action } = click;
|
|
16213
|
+
return {
|
|
16214
|
+
type: "message",
|
|
16215
|
+
id,
|
|
16216
|
+
sender: click.sender,
|
|
16217
|
+
conversation: action.conversation,
|
|
16218
|
+
locale: action.locale,
|
|
16219
|
+
clientPlatform: action.clientPlatform,
|
|
16220
|
+
text: text8,
|
|
16221
|
+
mentioned: action.conversation.kind !== "personal",
|
|
16222
|
+
targeted: false,
|
|
16223
|
+
namesBotFirst: false,
|
|
16224
|
+
conversationName: "",
|
|
16225
|
+
mentions: [],
|
|
16226
|
+
links: [],
|
|
16227
|
+
quotes: [],
|
|
16228
|
+
files: [],
|
|
16229
|
+
unreadableFiles: [],
|
|
16230
|
+
provenance: "typed",
|
|
16231
|
+
choice: true
|
|
16232
|
+
// (an option that is a menu word or a help phrase is an answer, never that command)
|
|
16233
|
+
};
|
|
16234
|
+
}
|
|
16235
|
+
function langOf3(card, click) {
|
|
16236
|
+
if (card?.lang === "zh" || card?.lang === "en") return card.lang;
|
|
16237
|
+
return click.lang();
|
|
16238
|
+
}
|
|
16239
|
+
var AssistantChoiceClicks = class {
|
|
16240
|
+
constructor(deps) {
|
|
16241
|
+
this.deps = deps;
|
|
16242
|
+
}
|
|
16243
|
+
deps;
|
|
16244
|
+
verbs = [CHOICE_VERB];
|
|
16245
|
+
/** A 1:1 chat: answered with no Claw key (the message it sends is checked as any typed one is). */
|
|
16246
|
+
async beforeCredential(click) {
|
|
16247
|
+
await this.press(click);
|
|
16248
|
+
return true;
|
|
16249
|
+
}
|
|
16250
|
+
async inGroup(click) {
|
|
16251
|
+
await this.press(click);
|
|
16252
|
+
return true;
|
|
16253
|
+
}
|
|
16254
|
+
async press(click) {
|
|
16255
|
+
const buttons2 = this.deps.store.assistantButtons;
|
|
16256
|
+
const card = buttons2.getChoiceCard(pyStr(click.action.data["card"]));
|
|
16257
|
+
const lang = langOf3(card, click);
|
|
16258
|
+
const t4 = assistantCardText.in(lang);
|
|
16259
|
+
if (card === null || click.conversation.ref.kind === "personal" && card.userId !== click.sender.id) {
|
|
16260
|
+
await finish(click, t4.choiceGone);
|
|
16261
|
+
return;
|
|
16262
|
+
}
|
|
16263
|
+
if (card.state === "done") {
|
|
16264
|
+
await this.sent(click, card, card.chosen, lang, false);
|
|
16265
|
+
return;
|
|
16266
|
+
}
|
|
16267
|
+
if (card.state !== "open") {
|
|
16268
|
+
await click.settle(t4.choiceSending({ answer: card.chosen.join(t4.choiceJoin) }), { announce: false });
|
|
16269
|
+
return;
|
|
16270
|
+
}
|
|
16271
|
+
const chosen = pickedOn(card, click.action.data[CHOICE_INPUT]);
|
|
16272
|
+
if (!chosen.length || !card.multiple && chosen.length > 1) {
|
|
16273
|
+
await this.again(click, card, lang, t4.choiceNone);
|
|
16274
|
+
return;
|
|
16275
|
+
}
|
|
16276
|
+
if (!buttons2.claimChoiceCard(card.cardId, chosen)) {
|
|
16277
|
+
const now = buttons2.getChoiceCard(card.cardId) ?? card;
|
|
16278
|
+
if (now.state === "done") await this.sent(click, now, now.chosen, lang, false);
|
|
16279
|
+
else await click.settle(t4.choiceSending({ answer: now.chosen.join(t4.choiceJoin) }), { announce: false });
|
|
16280
|
+
return;
|
|
16281
|
+
}
|
|
16282
|
+
const text8 = chosen.join(t4.choiceJoin);
|
|
16283
|
+
try {
|
|
16284
|
+
await this.deps.deliver(typedMessage(click, text8, `${CHOICE_VERB}:${card.cardId}`));
|
|
16285
|
+
} catch (error) {
|
|
16286
|
+
buttons2.moveChoiceCard(card.cardId, "open", { onlyFrom: "busy" });
|
|
16287
|
+
this.deps.logger.warn("a choice could not be sent; its card is given back", { error: errorName(error) });
|
|
16288
|
+
await this.again(click, card, lang, t4.choiceFailed);
|
|
16289
|
+
return;
|
|
16290
|
+
}
|
|
16291
|
+
buttons2.moveChoiceCard(card.cardId, "done", { onlyFrom: "busy" });
|
|
16292
|
+
await this.sent(click, card, chosen, lang, true);
|
|
16293
|
+
}
|
|
16294
|
+
/** The card back, open, with `note` under its options. */
|
|
16295
|
+
async again(click, card, lang, note) {
|
|
16296
|
+
const model = { ...this.model(card, lang), note };
|
|
16297
|
+
await click.settle(viewMessage2({ name: "choiceCard", model }), { announce: false });
|
|
16298
|
+
}
|
|
16299
|
+
/** The card replaced by what was picked and sent (said as a line too when `announce`). */
|
|
16300
|
+
async sent(click, card, chosen, lang, announce) {
|
|
16301
|
+
const t4 = assistantCardText.in(lang);
|
|
16302
|
+
const view7 = viewMessage2({ name: "choiceChosen", model: { text: card.text, chosen, lang } });
|
|
16303
|
+
await click.settle(
|
|
16304
|
+
view7,
|
|
16305
|
+
announce ? { fallback: t4.choiceSent({ answer: chosen.join(t4.choiceJoin) }) } : { announce: false }
|
|
16306
|
+
);
|
|
16307
|
+
}
|
|
16308
|
+
model(card, lang) {
|
|
16309
|
+
return { text: card.text, cardId: card.cardId, options: card.options, multiple: card.multiple, note: "", lang };
|
|
16310
|
+
}
|
|
16311
|
+
};
|
|
16312
|
+
|
|
15936
16313
|
// packages/core/src/assistant/auto-reply.ts
|
|
15937
16314
|
function answersUnasked(watch) {
|
|
15938
16315
|
return watch["state"] === "active" && watch["reply_allowed"] === true;
|
|
@@ -16941,6 +17318,24 @@ function suggestedRepliesOf(event, text8, styleOn, lang) {
|
|
|
16941
17318
|
if (replies.length) return replies.slice(0, SUGGESTIONS_MAX);
|
|
16942
17319
|
return [...ASSISTANT_SUGGESTED_REPLIES[lang ?? pickLang2(text8)]];
|
|
16943
17320
|
}
|
|
17321
|
+
var CHOICE_OPTIONS_MIN = 2;
|
|
17322
|
+
var CHOICE_OPTIONS_MAX = 10;
|
|
17323
|
+
var CHOICE_OPTION_MAX = 80;
|
|
17324
|
+
function choiceOf(event) {
|
|
17325
|
+
const choice = record2(event["choices"]);
|
|
17326
|
+
if (event["type"] !== "assistant.say" || choice === void 0) return void 0;
|
|
17327
|
+
const raw = choice["options"];
|
|
17328
|
+
if (!Array.isArray(raw)) return void 0;
|
|
17329
|
+
const options = [];
|
|
17330
|
+
for (const entry of raw) {
|
|
17331
|
+
if (typeof entry !== "string" || /[\u0000-\u001f\u007f\u2028\u2029]/u.test(entry)) return void 0;
|
|
17332
|
+
const option = pySplit(entry).join(" ");
|
|
17333
|
+
if (!option || cpLength(option) > CHOICE_OPTION_MAX) return void 0;
|
|
17334
|
+
if (!options.includes(option)) options.push(option);
|
|
17335
|
+
}
|
|
17336
|
+
if (options.length < CHOICE_OPTIONS_MIN || options.length > CHOICE_OPTIONS_MAX) return void 0;
|
|
17337
|
+
return { options, multiple: choice["multiple"] === true };
|
|
17338
|
+
}
|
|
16944
17339
|
var REMOTE_KINDS = /* @__PURE__ */ new Set(["done", "needs_you", "error", "resolved"]);
|
|
16945
17340
|
function resolvedOf(remote) {
|
|
16946
17341
|
const resolved = record2(remote["resolved"]);
|
|
@@ -17077,14 +17472,19 @@ ${text8}`;
|
|
|
17077
17472
|
return all.length ? [...said2.slice(0, -1), { ...last, suggestions: toReplies(all.slice(0, SUGGESTIONS_MAX)) }] : said2;
|
|
17078
17473
|
}
|
|
17079
17474
|
/**
|
|
17080
|
-
* The card with buttons the event asks for, recorded first: a result that waits, a write Claw holds, or a
|
|
17081
|
-
* person; undefined for none. `holdsWords`: the card shows the words, which are then not said apart.
|
|
17475
|
+
* The card with buttons the event asks for, recorded first: a result that waits, a write Claw holds, a command or a choice put
|
|
17476
|
+
* to the person; undefined for none. `holdsWords`: the card shows the words, which are then not said apart.
|
|
17082
17477
|
*/
|
|
17083
17478
|
card(userId, event, item, said2) {
|
|
17084
17479
|
const inCard = said2.inCard ? said2.text : "";
|
|
17085
17480
|
if (item?.canAccept)
|
|
17086
17481
|
return { message: this.workCard(userId, item, said2.text, inCard, said2.lang), holdsWords: said2.inCard };
|
|
17087
|
-
|
|
17482
|
+
const asked3 = item === null ? this.askCard(userId, event, inCard, said2.lang) : void 0;
|
|
17483
|
+
if (asked3 !== void 0) return { message: asked3, holdsWords: said2.inCard };
|
|
17484
|
+
return this.choiceCard(userId, event, inCard, said2.lang, said2.inCard);
|
|
17485
|
+
}
|
|
17486
|
+
/** A write Claw holds for the person (Allow, Deny) or a command put to them (Run, Cancel), recorded first; undefined for none. */
|
|
17487
|
+
askCard(userId, event, inCard, lang) {
|
|
17088
17488
|
const held = confirmWriteOf(event);
|
|
17089
17489
|
if (held !== void 0) {
|
|
17090
17490
|
const cardId2 = this.deps.store.assistantButtons.addConfirmCard(
|
|
@@ -17093,8 +17493,8 @@ ${text8}`;
|
|
|
17093
17493
|
held.kind,
|
|
17094
17494
|
held.allowLabel
|
|
17095
17495
|
);
|
|
17096
|
-
const model = { text: inCard || held.allowLabel, cardId: cardId2, allowLabel: held.allowLabel, lang
|
|
17097
|
-
return
|
|
17496
|
+
const model = { text: inCard || held.allowLabel, cardId: cardId2, allowLabel: held.allowLabel, lang };
|
|
17497
|
+
return view3({ name: "confirmWrite", model });
|
|
17098
17498
|
}
|
|
17099
17499
|
const confirm = confirmActionOf(event, this.deps.exactCommand);
|
|
17100
17500
|
if (confirm === void 0) return void 0;
|
|
@@ -17105,10 +17505,19 @@ ${text8}`;
|
|
|
17105
17505
|
confirm.name,
|
|
17106
17506
|
confirm.args
|
|
17107
17507
|
);
|
|
17108
|
-
return {
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17508
|
+
return view3({ name: "actionCard", model: { text: inCard || command, cardId, command, lang } });
|
|
17509
|
+
}
|
|
17510
|
+
/**
|
|
17511
|
+
* A choice put to the person (`choices`): its options and Send, recorded first (what a press sends is read from the record).
|
|
17512
|
+
* Its Send sends what was picked as the person's own message.
|
|
17513
|
+
*/
|
|
17514
|
+
choiceCard(userId, event, inCard, lang, holdsWords) {
|
|
17515
|
+
const choice = choiceOf(event);
|
|
17516
|
+
if (choice === void 0) return void 0;
|
|
17517
|
+
const { options, multiple } = choice;
|
|
17518
|
+
const cardId = this.deps.store.assistantButtons.addChoiceCard(userId, { text: inCard, options, multiple, lang });
|
|
17519
|
+
const model = { text: inCard, cardId, options, multiple, note: "", lang };
|
|
17520
|
+
return { message: view3({ name: "choiceCard", model }, inCard), holdsWords };
|
|
17112
17521
|
}
|
|
17113
17522
|
/** What was said, as AI answers with thumbs, each with the citations its own words cite (when the whole text cites at all). */
|
|
17114
17523
|
said(event, text8, parts2) {
|
|
@@ -32555,6 +32964,7 @@ export {
|
|
|
32555
32964
|
AssistantActionClicks,
|
|
32556
32965
|
AssistantActions,
|
|
32557
32966
|
AssistantChat as AssistantChatService,
|
|
32967
|
+
AssistantChoiceClicks,
|
|
32558
32968
|
BOARD_CARDS,
|
|
32559
32969
|
BOARD_LINES,
|
|
32560
32970
|
BOARD_RETRY_MAX,
|
|
@@ -32844,6 +33254,7 @@ export {
|
|
|
32844
33254
|
casefold,
|
|
32845
33255
|
channelActions,
|
|
32846
33256
|
channelContext,
|
|
33257
|
+
chatCredentialOf,
|
|
32847
33258
|
chatTurnKey,
|
|
32848
33259
|
clawText,
|
|
32849
33260
|
cleanText,
|
|
@@ -32947,6 +33358,7 @@ export {
|
|
|
32947
33358
|
lastTurns,
|
|
32948
33359
|
learnLang,
|
|
32949
33360
|
limitsText,
|
|
33361
|
+
logTurnStarted,
|
|
32950
33362
|
looksInternal,
|
|
32951
33363
|
looksLikeFailure,
|
|
32952
33364
|
looksLikeRepo,
|
|
@@ -33147,6 +33559,7 @@ export {
|
|
|
33147
33559
|
turnIdOf,
|
|
33148
33560
|
two,
|
|
33149
33561
|
typedAnswerText,
|
|
33562
|
+
typedAnswers,
|
|
33150
33563
|
typedMode,
|
|
33151
33564
|
unique,
|
|
33152
33565
|
unreachable,
|