@primusclaw/personal-agent 0.4.0 → 0.5.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 +945 -741
- 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/receipts.d.ts +1 -1
- package/dist/types/locales/approval.d.ts +11 -0
- package/dist/types/locales/turn.d.ts +2 -0
- package/dist/types/ports/store/approvals.d.ts +24 -2
- package/dist/types/ports/surface.d.ts +5 -2
- 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 +1 -1
- package/dist/types/routing/question-reply.d.ts +22 -0
- package/dist/types/routing/turn-start.d.ts +19 -0
- package/dist/types/turn/engine/context.d.ts +4 -2
- 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
|
|
|
@@ -5746,6 +5757,8 @@ var turnText = defineCatalog({
|
|
|
5746
5757
|
thinking: "Thinking\u2026",
|
|
5747
5758
|
/** A turn still running when its stream has to close: the answer follows as a message of its own. */
|
|
5748
5759
|
stillWorking: "Still working on it; the result will follow as a new message.",
|
|
5760
|
+
/** A turn whose stream is closed because a card now waits for the person's choice. */
|
|
5761
|
+
awaitingChoice: "Waiting for your choice in the card; I will continue once you pick.",
|
|
5749
5762
|
interrupted: "Interrupted.",
|
|
5750
5763
|
/** What a turn the person stopped ends with. */
|
|
5751
5764
|
stopped: "Stopped.",
|
|
@@ -5767,6 +5780,7 @@ var turnText = defineCatalog({
|
|
|
5767
5780
|
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
5781
|
thinking: "\u601D\u8003\u4E2D\u2026",
|
|
5769
5782
|
stillWorking: "\u4ECD\u5728\u5904\u7406\uFF0C\u5B8C\u6210\u540E\u53E6\u53D1\u7ED3\u679C\u3002",
|
|
5783
|
+
awaitingChoice: "\u8BF7\u5728\u5361\u7247\u91CC\u9009\u62E9\uFF0C\u9009\u597D\u540E\u6211\u63A5\u7740\u505A\u3002",
|
|
5770
5784
|
interrupted: "\u5DF2\u4E2D\u65AD\u3002",
|
|
5771
5785
|
stopped: "\u5DF2\u505C\u6B62\u3002",
|
|
5772
5786
|
doneShort: "\u5B8C\u6210\u3002",
|
|
@@ -6949,6 +6963,8 @@ var StatusMessage = class {
|
|
|
6949
6963
|
opening;
|
|
6950
6964
|
expiry;
|
|
6951
6965
|
again;
|
|
6966
|
+
/** The stream being closed because a card waits for the person (`awaitingChoice`). */
|
|
6967
|
+
choice;
|
|
6952
6968
|
preparingTimer;
|
|
6953
6969
|
lastText = "";
|
|
6954
6970
|
lastAt = 0;
|
|
@@ -7027,20 +7043,61 @@ ${header}` : header;
|
|
|
7027
7043
|
const live = this.live;
|
|
7028
7044
|
if (!live) return;
|
|
7029
7045
|
await sleepSeconds(this.clock, Math.max(STREAM_CLOSE_SECONDS - live.age(), 0), signal);
|
|
7030
|
-
|
|
7046
|
+
await this.closeEarly(live, turnText.in(this.lang).stillWorking, "before the chat's limit", signal);
|
|
7047
|
+
}
|
|
7048
|
+
/**
|
|
7049
|
+
* 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
|
|
7050
|
+
* a message of its own. Nothing more is sent to the stream once this has begun.
|
|
7051
|
+
*/
|
|
7052
|
+
async closeEarly(live, line2, why, signal) {
|
|
7053
|
+
if (this.closed || !live.closable || this.streamOver || this.expiring) return;
|
|
7031
7054
|
this.expiring = true;
|
|
7032
7055
|
if (this.stream && this.flusher && !this.flusher.done) await unlessAborted(this.flusher.settled(), signal);
|
|
7033
7056
|
this.streamOver = true;
|
|
7034
|
-
const
|
|
7035
|
-
const closed = await live.final(plainMessage(
|
|
7057
|
+
const words3 = this.streamable() || live.streamed;
|
|
7058
|
+
const closed = await live.final(plainMessage(words3 ? `${words3}
|
|
7036
7059
|
|
|
7037
7060
|
${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");
|
|
7061
|
+
if (!closed.ok) this.log.warn(`the turn's stream could not be closed ${why}; the answer follows as a message`);
|
|
7040
7062
|
this.log.info("the turn's stream was closed; the answer follows as a message of its own", {
|
|
7063
|
+
why,
|
|
7041
7064
|
age: Math.round(live.age())
|
|
7042
7065
|
});
|
|
7043
7066
|
}
|
|
7067
|
+
/**
|
|
7068
|
+
* A card now waits for the person (a question of the agent): it is their turn, so the floating status must not go on
|
|
7069
|
+
* saying "Thinking…" until the chat's limit. An open stream is closed at once with AWAITING_CHOICE, and the answer comes
|
|
7070
|
+
* as a message of its own, as after the limit. Without a stream (the progress message) nothing changes: it has no
|
|
7071
|
+
* spinner. Never fails the turn.
|
|
7072
|
+
*/
|
|
7073
|
+
awaitingChoice() {
|
|
7074
|
+
const live = this.live;
|
|
7075
|
+
if (this.closed || !live || this.streamOver || this.expiring || this.choice) return;
|
|
7076
|
+
this.choice = detach(async (signal) => {
|
|
7077
|
+
if (this.opening && !this.opening.done) await unlessAborted(this.opening.settled(), signal);
|
|
7078
|
+
if (!this.expiring) this.expiry?.cancel();
|
|
7079
|
+
await this.closeEarly(live, turnText.in(this.lang).awaitingChoice, "while a card waits for the person", signal);
|
|
7080
|
+
});
|
|
7081
|
+
}
|
|
7082
|
+
/**
|
|
7083
|
+
* Claw queued the message behind a turn it is still running: `notice` closes the stream at once, so it does not say
|
|
7084
|
+
* "Thinking…" for the whole wait. The answer comes as a message of its own, as after the limit. When the stream did not open
|
|
7085
|
+
* after all, `notice` is said as a message. False when there is no stream: the caller says it. Never fails the turn.
|
|
7086
|
+
*/
|
|
7087
|
+
queued(notice) {
|
|
7088
|
+
const live = this.live;
|
|
7089
|
+
if (this.closed || !live || this.streamOver || this.expiring || this.choice) return false;
|
|
7090
|
+
this.choice = detach(async (signal) => {
|
|
7091
|
+
if (this.opening && !this.opening.done) await unlessAborted(this.opening.settled(), signal);
|
|
7092
|
+
if (this.closed || !live.closable || this.streamOver || this.expiring) {
|
|
7093
|
+
await this.surface.send(plainMessage(notice));
|
|
7094
|
+
return;
|
|
7095
|
+
}
|
|
7096
|
+
this.expiry?.cancel();
|
|
7097
|
+
await this.closeEarly(live, notice, "while the message waits behind another turn", signal);
|
|
7098
|
+
});
|
|
7099
|
+
return true;
|
|
7100
|
+
}
|
|
7044
7101
|
/** The chat's id of the posted message; undefined when the post failed or its id never came (nothing is edited then). */
|
|
7045
7102
|
async messageId(signal) {
|
|
7046
7103
|
if (!this.posted) return void 0;
|
|
@@ -7185,8 +7242,7 @@ ${line2}` : line2));
|
|
|
7185
7242
|
this.hard = this.hard || hard;
|
|
7186
7243
|
this.stopPreparingTimer();
|
|
7187
7244
|
if (this.flusher && !this.flusher.done && (hard || !this.sending)) this.flusher.cancel();
|
|
7188
|
-
if (hard) this.again?.cancel();
|
|
7189
|
-
if (hard) this.expiry?.cancel();
|
|
7245
|
+
if (hard) for (const task of [this.again, this.expiry, this.choice]) task?.cancel();
|
|
7190
7246
|
if (hard && this.live?.closable && !this.streamOver) {
|
|
7191
7247
|
this.streamOver = true;
|
|
7192
7248
|
const live = this.live;
|
|
@@ -7220,7 +7276,7 @@ ${line2}` : line2));
|
|
|
7220
7276
|
async quiesce() {
|
|
7221
7277
|
this.close();
|
|
7222
7278
|
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]) {
|
|
7279
|
+
for (const task of [this.opening, this.flusher, this.expiring ? this.expiry : void 0, this.choice]) {
|
|
7224
7280
|
if (task) await task.settled();
|
|
7225
7281
|
}
|
|
7226
7282
|
}
|
|
@@ -7992,15 +8048,17 @@ async function ask2(ctx, sessionId, step) {
|
|
|
7992
8048
|
const asked3 = isObject3(step.approval) && truthy(step.approval) ? approvalOf(ctx, sessionId, step.approval) : questionOf2(ctx, sessionId, step.question ?? {});
|
|
7993
8049
|
const { tool: tool2, toolUseId, request, envelope } = asked3;
|
|
7994
8050
|
const hooks = ctx.hooks;
|
|
7995
|
-
|
|
8051
|
+
const questions = tool2 === QUESTION_TOOL ? request["questions"] : void 0;
|
|
8052
|
+
if (toolUseId && hooks.onApproval?.(sessionId, toolUseId, tool2, waitSeconds(request["timeout_ms"]), envelope, questions) === false)
|
|
7996
8053
|
return;
|
|
8054
|
+
let cardId;
|
|
7997
8055
|
try {
|
|
7998
|
-
await ctx.deps.surface.deliver(envelope);
|
|
8056
|
+
cardId = await ctx.deps.surface.deliver(envelope);
|
|
7999
8057
|
} catch (err2) {
|
|
8000
8058
|
if (toolUseId) hooks.onApprovalUnsent?.(sessionId, toolUseId);
|
|
8001
8059
|
throw err2;
|
|
8002
8060
|
}
|
|
8003
|
-
if (toolUseId) hooks.onApprovalSent?.(sessionId, toolUseId);
|
|
8061
|
+
if (toolUseId) hooks.onApprovalSent?.(sessionId, toolUseId, cardId);
|
|
8004
8062
|
}
|
|
8005
8063
|
async function readOnce(ctx, args, cursor, told) {
|
|
8006
8064
|
const { sessionId, state, status, signal } = args;
|
|
@@ -8032,6 +8090,7 @@ async function take(ctx, args, step) {
|
|
|
8032
8090
|
args.status.progress(step);
|
|
8033
8091
|
if (truthy(step.approval) || truthy(step.question)) {
|
|
8034
8092
|
await ask2(ctx, args.sessionId, step);
|
|
8093
|
+
if (!truthy(step.approval)) args.status.awaitingChoice();
|
|
8035
8094
|
return void 0;
|
|
8036
8095
|
}
|
|
8037
8096
|
if (!step.finished) return void 0;
|
|
@@ -8418,8 +8477,9 @@ var TurnRun = class {
|
|
|
8418
8477
|
state.writes = ctx.record.wrote;
|
|
8419
8478
|
this.state = state;
|
|
8420
8479
|
noteRunning(ctx, state, this.running);
|
|
8421
|
-
if (messageId
|
|
8422
|
-
|
|
8480
|
+
if (queuedByClaw(messageId) && !ctx.request.attach) {
|
|
8481
|
+
const notice = turnText.in(langOf(ctx)).queuedNotice;
|
|
8482
|
+
if (!this.status.queued(notice)) await ctx.deps.surface.send(plainMessage(notice));
|
|
8423
8483
|
}
|
|
8424
8484
|
return consume(ctx, { sessionId, state, status: this.status, running: this.running, signal });
|
|
8425
8485
|
}
|
|
@@ -8573,6 +8633,10 @@ var TurnRun = class {
|
|
|
8573
8633
|
await offerReport(ctx);
|
|
8574
8634
|
}
|
|
8575
8635
|
};
|
|
8636
|
+
var QUEUED_MESSAGE_PREFIX = "claw-pending-";
|
|
8637
|
+
function queuedByClaw(messageId) {
|
|
8638
|
+
return messageId === void 0 || messageId.startsWith(QUEUED_MESSAGE_PREFIX);
|
|
8639
|
+
}
|
|
8576
8640
|
async function runTurn(ctx, controls) {
|
|
8577
8641
|
const replyTo = streamReplyTo(ctx);
|
|
8578
8642
|
if (!replyTo) detach(() => typing(ctx));
|
|
@@ -8782,9 +8846,9 @@ var TurnEngine = class {
|
|
|
8782
8846
|
const review = run.review?.turn;
|
|
8783
8847
|
return {
|
|
8784
8848
|
...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);
|
|
8849
|
+
onApproval: (sid, toolUseId, tool2, expiresIn, envelope, questions) => approvals.record(run.userId, sid, toolUseId, tool2, expiresIn, envelope, questions),
|
|
8850
|
+
onApprovalSent: (sid, toolUseId, cardId) => {
|
|
8851
|
+
approvals.sent(sid, toolUseId, cardId);
|
|
8788
8852
|
},
|
|
8789
8853
|
onApprovalUnsent: (sid, toolUseId) => {
|
|
8790
8854
|
approvals.unsent(sid, toolUseId);
|
|
@@ -9697,6 +9761,14 @@ var CredentialUnavailable = class extends Error {
|
|
|
9697
9761
|
};
|
|
9698
9762
|
var IDENTITY_CARD_INTERVAL_SECONDS = 24 * 3600;
|
|
9699
9763
|
var IDENTITY_CARD_NOTICE = "identity_card";
|
|
9764
|
+
function chatCredentialOf(links2, exchange) {
|
|
9765
|
+
return async (sender) => {
|
|
9766
|
+
const key = links2.getKey(sender.id) ?? void 0;
|
|
9767
|
+
if (exchange === void 0 || !exchange.enabled) return key;
|
|
9768
|
+
const token = await exchange.tokenFor(sender.id);
|
|
9769
|
+
return token.ok ? token.value : key;
|
|
9770
|
+
};
|
|
9771
|
+
}
|
|
9700
9772
|
var Credentials = class {
|
|
9701
9773
|
constructor(deps, views) {
|
|
9702
9774
|
this.deps = deps;
|
|
@@ -13273,162 +13345,674 @@ var GroupRemote = class {
|
|
|
13273
13345
|
}
|
|
13274
13346
|
};
|
|
13275
13347
|
|
|
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
|
-
}
|
|
13348
|
+
// packages/core/src/approvals/questions.ts
|
|
13349
|
+
import { z as z4 } from "zod";
|
|
13350
|
+
|
|
13351
|
+
// packages/core/src/text/plain.ts
|
|
13352
|
+
function clip3(text8, limit) {
|
|
13353
|
+
return clipLine(squashSpaces(text8), limit);
|
|
13354
|
+
}
|
|
13355
|
+
function pyJson(value, options = {}) {
|
|
13356
|
+
return options.compact ? compact(value) : pyJsonDumps(value);
|
|
13357
|
+
}
|
|
13358
|
+
function compact(value) {
|
|
13359
|
+
if (Array.isArray(value)) return `[${value.map(compact).join(",")}]`;
|
|
13360
|
+
if (value !== null && typeof value === "object") {
|
|
13361
|
+
const entries = Object.entries(value);
|
|
13362
|
+
return `{${entries.map(([k, v]) => `${pyJsonDumps(k)}:${compact(v)}`).join(",")}}`;
|
|
13376
13363
|
}
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13364
|
+
return pyJsonDumps(value);
|
|
13365
|
+
}
|
|
13366
|
+
|
|
13367
|
+
// packages/core/src/approvals/questions.ts
|
|
13368
|
+
var QUESTION_TOOL2 = "ask_user_question";
|
|
13369
|
+
var QUESTIONS_MAX = 5;
|
|
13370
|
+
var OPTIONS_MAX = 10;
|
|
13371
|
+
var QUESTION_MAX = 500;
|
|
13372
|
+
var OPTION_MAX = 120;
|
|
13373
|
+
var ANSWER_MAX = 1e3;
|
|
13374
|
+
function scalarText(value) {
|
|
13375
|
+
if (value === null || value === void 0 || value === false || value === 0 || value === "") return "";
|
|
13376
|
+
if (typeof value === "string") return value;
|
|
13377
|
+
if (typeof value === "number") return String(value);
|
|
13378
|
+
if (value === true) return "True";
|
|
13379
|
+
return "";
|
|
13380
|
+
}
|
|
13381
|
+
var rawOption = z4.object({ id: z4.unknown(), label: z4.unknown() }).partial();
|
|
13382
|
+
var rawQuestion = z4.object({ id: z4.unknown(), question: z4.unknown(), options: z4.unknown(), allow_multiple: z4.unknown() }).partial();
|
|
13383
|
+
function optionsOf(raw) {
|
|
13384
|
+
const out = [];
|
|
13385
|
+
for (const item of Array.isArray(raw) ? raw : []) {
|
|
13386
|
+
const option = rawOption.safeParse(item);
|
|
13387
|
+
if (!option.success || typeof item !== "object" || Array.isArray(item)) continue;
|
|
13388
|
+
const id = scalarText(option.data.id);
|
|
13389
|
+
if (!pyStrip(id)) continue;
|
|
13390
|
+
out.push({ id, label: clip3(scalarText(option.data.label) || id, OPTION_MAX) });
|
|
13384
13391
|
}
|
|
13385
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
const
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
|
|
13395
|
-
|
|
13396
|
-
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
await this.starter.start({
|
|
13401
|
-
chat,
|
|
13402
|
-
credential: key,
|
|
13403
|
-
text: said2.text,
|
|
13404
|
-
messageId: message.id,
|
|
13405
|
-
clientPlatform: message.clientPlatform,
|
|
13406
|
-
quote: said2.quote,
|
|
13407
|
-
sent: said2.sent,
|
|
13408
|
-
files: said2.files
|
|
13392
|
+
return out.slice(0, OPTIONS_MAX);
|
|
13393
|
+
}
|
|
13394
|
+
function cardQuestions(raw) {
|
|
13395
|
+
const out = [];
|
|
13396
|
+
for (const item of Array.isArray(raw) ? raw : []) {
|
|
13397
|
+
const question = rawQuestion.safeParse(item);
|
|
13398
|
+
if (!question.success || typeof item !== "object" || Array.isArray(item)) continue;
|
|
13399
|
+
const id = scalarText(question.data.id);
|
|
13400
|
+
const text8 = scalarText(question.data.question);
|
|
13401
|
+
if (!pyStrip(id) || !pyStrip(text8)) continue;
|
|
13402
|
+
out.push({
|
|
13403
|
+
id,
|
|
13404
|
+
question: clip3(text8, QUESTION_MAX),
|
|
13405
|
+
options: optionsOf(question.data.options),
|
|
13406
|
+
allowMultiple: question.data.allow_multiple === true
|
|
13409
13407
|
});
|
|
13410
13408
|
}
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
13417
|
-
|
|
13418
|
-
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13426
|
-
|
|
13427
|
-
|
|
13428
|
-
|
|
13429
|
-
}
|
|
13430
|
-
|
|
13431
|
-
|
|
13409
|
+
return out.slice(0, QUESTIONS_MAX);
|
|
13410
|
+
}
|
|
13411
|
+
function questionWire(question) {
|
|
13412
|
+
return {
|
|
13413
|
+
id: question.id,
|
|
13414
|
+
question: question.question,
|
|
13415
|
+
options: question.options.map((o) => ({ id: o.id, label: o.label })),
|
|
13416
|
+
allow_multiple: question.allowMultiple
|
|
13417
|
+
};
|
|
13418
|
+
}
|
|
13419
|
+
function answersOf(data) {
|
|
13420
|
+
const answers = /* @__PURE__ */ new Map();
|
|
13421
|
+
const skipped = [];
|
|
13422
|
+
cardQuestions(data["questions"]).forEach((q, i) => {
|
|
13423
|
+
const labels = new Map(q.options.map((o) => [o.id, o.label]));
|
|
13424
|
+
const chosen = scalarText(own(data, `choice_${String(i)}`)).split(",").flatMap((c) => {
|
|
13425
|
+
const label = labels.get(c);
|
|
13426
|
+
return label === void 0 ? [] : [label];
|
|
13427
|
+
});
|
|
13428
|
+
const typed2 = cpHead(pyStrip(scalarText(own(data, `text_${String(i)}`))), ANSWER_MAX);
|
|
13429
|
+
const answer2 = [chosen.join(", "), typed2].filter(Boolean).join(". ");
|
|
13430
|
+
if (answer2) answers.set(q.id, answer2);
|
|
13431
|
+
else skipped.push(q.id);
|
|
13432
|
+
});
|
|
13433
|
+
return { answers, skipped };
|
|
13434
|
+
}
|
|
13435
|
+
function optionNamed(options, token) {
|
|
13436
|
+
const wanted = token.toLowerCase();
|
|
13437
|
+
const byLabel = options.find((o) => o.label.toLowerCase() === wanted || o.id.toLowerCase() === wanted);
|
|
13438
|
+
if (byLabel !== void 0 || !/^\d{1,2}$/.test(token)) return byLabel;
|
|
13439
|
+
return options[Number(token) - 1];
|
|
13440
|
+
}
|
|
13441
|
+
function typedAnswers(questions, typed2) {
|
|
13442
|
+
const reply = cpHead(pyStrip(typed2), ANSWER_MAX);
|
|
13443
|
+
const answers = /* @__PURE__ */ new Map();
|
|
13444
|
+
const only2 = questions.length === 1 ? questions[0] : void 0;
|
|
13445
|
+
if (only2 !== void 0) {
|
|
13446
|
+
const tokens2 = only2.allowMultiple ? reply.split(/\s*[,,、;;]\s*/u).filter(Boolean) : [reply];
|
|
13447
|
+
const named2 = tokens2.flatMap((t4) => optionNamed(only2.options, t4) ?? []);
|
|
13448
|
+
const labels = named2.length > 0 && named2.length === tokens2.length ? named2.map((o) => o.label) : [];
|
|
13449
|
+
answers.set(only2.id, labels.length ? labels.join(", ") : reply);
|
|
13450
|
+
} else {
|
|
13451
|
+
for (const q of questions) answers.set(q.id, reply);
|
|
13452
|
+
}
|
|
13453
|
+
return { answers, skipped: [] };
|
|
13454
|
+
}
|
|
13455
|
+
|
|
13456
|
+
// packages/core/src/approvals/click.ts
|
|
13457
|
+
function viewMessage2(view7, fallbackText = "") {
|
|
13458
|
+
return { body: { type: "view", view: view7, fallbackText } };
|
|
13459
|
+
}
|
|
13460
|
+
|
|
13461
|
+
// packages/core/src/proactive/quiet.ts
|
|
13462
|
+
var APPROVAL_DEFAULT_SECONDS = 600;
|
|
13463
|
+
var PERMISSION_DEFAULT_SECONDS = 600;
|
|
13464
|
+
var PERMISSION_MAX_SECONDS = 3600;
|
|
13465
|
+
var formatters = /* @__PURE__ */ new Map();
|
|
13466
|
+
function formatter(zone) {
|
|
13467
|
+
const known = formatters.get(zone);
|
|
13468
|
+
if (known) return known;
|
|
13469
|
+
try {
|
|
13470
|
+
const made = new Intl.DateTimeFormat("en-US", {
|
|
13471
|
+
timeZone: zone,
|
|
13472
|
+
hour: "2-digit",
|
|
13473
|
+
minute: "2-digit",
|
|
13474
|
+
hourCycle: "h23"
|
|
13475
|
+
});
|
|
13476
|
+
formatters.set(zone, made);
|
|
13477
|
+
return made;
|
|
13478
|
+
} catch {
|
|
13479
|
+
return void 0;
|
|
13480
|
+
}
|
|
13481
|
+
}
|
|
13482
|
+
function zoneOf3(prefs) {
|
|
13483
|
+
return prefs.timezone && formatter(prefs.timezone) ? prefs.timezone : DEFAULT_TIMEZONE;
|
|
13484
|
+
}
|
|
13485
|
+
function localMinutes(epochSeconds2, zone) {
|
|
13486
|
+
const parts2 = (formatter(zone) ?? formatter(DEFAULT_TIMEZONE))?.formatToParts(new Date(epochSeconds2 * 1e3)) ?? [];
|
|
13487
|
+
const value = (type) => Number(parts2.find((p) => p.type === type)?.value ?? "0");
|
|
13488
|
+
return value("hour") * 60 + value("minute");
|
|
13489
|
+
}
|
|
13490
|
+
function minutesOf(clock2) {
|
|
13491
|
+
const [hour = "0", minute = "0"] = clock2.split(":");
|
|
13492
|
+
return Number(hour) * 60 + Number(minute);
|
|
13493
|
+
}
|
|
13494
|
+
function inQuiet(now, zone, start, end) {
|
|
13495
|
+
const minute = localMinutes(now, zone);
|
|
13496
|
+
const s = minutesOf(start);
|
|
13497
|
+
const e = minutesOf(end);
|
|
13498
|
+
if (s === e) return false;
|
|
13499
|
+
return s < e ? s <= minute && minute < e : minute >= s || minute < e;
|
|
13500
|
+
}
|
|
13501
|
+
function muted(prefs, now) {
|
|
13502
|
+
if (prefs.pausedUntil !== null && now < prefs.pausedUntil) return "paused";
|
|
13503
|
+
if (prefs.quietEnabled && inQuiet(now, zoneOf3(prefs), prefs.quietStart, prefs.quietEnd)) return "quiet";
|
|
13504
|
+
return void 0;
|
|
13505
|
+
}
|
|
13506
|
+
function isoSeconds2(raw) {
|
|
13507
|
+
if (typeof raw !== "string") return void 0;
|
|
13508
|
+
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(
|
|
13509
|
+
raw.trim()
|
|
13510
|
+
);
|
|
13511
|
+
if (!match) return void 0;
|
|
13512
|
+
const at = Date.parse(raw.trim().replace(" ", "T"));
|
|
13513
|
+
return Number.isNaN(at) ? void 0 : at / 1e3;
|
|
13514
|
+
}
|
|
13515
|
+
function approvalExpiry(event, now) {
|
|
13516
|
+
const approval = event["approval"];
|
|
13517
|
+
const raw = typeof approval === "object" && approval !== null ? approval["expires_at"] : void 0;
|
|
13518
|
+
return isoSeconds2(raw) ?? now + APPROVAL_DEFAULT_SECONDS;
|
|
13519
|
+
}
|
|
13520
|
+
function remoteAskExpiry(ask3, now) {
|
|
13521
|
+
const at = isoSeconds2(ask3["expires_at"]);
|
|
13522
|
+
return at === void 0 ? now + PERMISSION_DEFAULT_SECONDS : Math.min(at, now + PERMISSION_MAX_SECONDS);
|
|
13523
|
+
}
|
|
13524
|
+
|
|
13525
|
+
// packages/core/src/routing/question-reply.ts
|
|
13526
|
+
var TYPED_ANSWER_MARGIN_SECONDS = 15;
|
|
13527
|
+
async function answerWaitingQuestion(ctx, reply) {
|
|
13528
|
+
const { store, claw, logger } = ctx.deps;
|
|
13529
|
+
const { chat } = reply;
|
|
13530
|
+
const waiting = waitingQuestion(ctx, reply);
|
|
13531
|
+
if (waiting === void 0) return false;
|
|
13532
|
+
const { pending, questions } = waiting;
|
|
13533
|
+
const { sessionId, toolUseId } = pending;
|
|
13534
|
+
const key = await stillAsked(ctx, reply, pending);
|
|
13535
|
+
if (key === void 0) return false;
|
|
13536
|
+
if (!store.approvals.closeApproval(sessionId, toolUseId, "sending")) return false;
|
|
13537
|
+
const answers = typedAnswers(questions, reply.text);
|
|
13538
|
+
try {
|
|
13539
|
+
await claw.sessions.answer(key, sessionId, toolUseId, Object.fromEntries(answers.answers), answers.skipped);
|
|
13540
|
+
} catch (error) {
|
|
13541
|
+
if (!isClawOrNetwork(error)) throw error;
|
|
13542
|
+
const stale = error instanceof ClawError && [404, 409, 410].includes(error.status);
|
|
13543
|
+
store.approvals.closeApproval(sessionId, toolUseId, stale ? "expired" : "pending", { onlyIfPending: false });
|
|
13544
|
+
logger.warn("a typed answer to a question card was not taken; the message goes on as a new one", {
|
|
13545
|
+
error: statusOrKind(error)
|
|
13546
|
+
});
|
|
13547
|
+
return false;
|
|
13548
|
+
}
|
|
13549
|
+
store.approvals.closeApproval(sessionId, toolUseId, "decided", { onlyIfPending: false });
|
|
13550
|
+
logger.info("a typed message answered the question card waiting in the chat", {
|
|
13551
|
+
chat: chat.scope ? "group" : "1:1",
|
|
13552
|
+
questions: questions.length,
|
|
13553
|
+
card: pending.cardId ? "edited" : "not editable"
|
|
13554
|
+
});
|
|
13555
|
+
await settleCard(ctx, chat, pending.cardId, [...answers.answers], pickLang2(reply.text, questions[0]?.question));
|
|
13556
|
+
return true;
|
|
13557
|
+
}
|
|
13558
|
+
function waitingQuestion(ctx, reply) {
|
|
13559
|
+
const { store } = ctx.deps;
|
|
13560
|
+
const { chat } = reply;
|
|
13561
|
+
if (!pyStrip(reply.text)) return void 0;
|
|
13562
|
+
const sessionId = store.sessions.getSession(chat.sender.id, chat.scope);
|
|
13563
|
+
if (sessionId === null || store.reviewDelegation.isActingSession(sessionId)) return void 0;
|
|
13564
|
+
const pending = store.approvals.pendingQuestion(sessionId, chat.sender.id);
|
|
13565
|
+
if (pending === null || pending.expiresAt - store.now() < TYPED_ANSWER_MARGIN_SECONDS) return void 0;
|
|
13566
|
+
const questions = cardQuestions(pending.questions);
|
|
13567
|
+
return questions.length === 0 ? void 0 : { pending, questions };
|
|
13568
|
+
}
|
|
13569
|
+
async function stillAsked(ctx, reply, pending) {
|
|
13570
|
+
const { store, claw, logger } = ctx.deps;
|
|
13571
|
+
try {
|
|
13572
|
+
const key = await reply.credential();
|
|
13573
|
+
if ((await claw.sessions.turn(key, pending.sessionId)).running) return key;
|
|
13574
|
+
store.approvals.closeApproval(pending.sessionId, pending.toolUseId, "expired");
|
|
13575
|
+
return void 0;
|
|
13576
|
+
} catch (error) {
|
|
13577
|
+
if (!isClawOrNetwork(error)) throw error;
|
|
13578
|
+
logger.warn("a typed answer to a question card was not sent: the session's turn could not be read", {
|
|
13579
|
+
error: statusOrKind(error)
|
|
13580
|
+
});
|
|
13581
|
+
return void 0;
|
|
13582
|
+
}
|
|
13583
|
+
}
|
|
13584
|
+
async function settleCard(ctx, chat, cardId, answers, lang) {
|
|
13585
|
+
const { store, logger } = ctx.deps;
|
|
13586
|
+
const zone = zoneOf3(store.prefs.getPrefs(chat.sender.id));
|
|
13587
|
+
const answer2 = [...new Set(answers.map(([, a]) => a))].join(" / ");
|
|
13588
|
+
const words3 = approvalText.in(lang).questionReplied({ answer: answer2 });
|
|
13589
|
+
try {
|
|
13590
|
+
if (cardId) {
|
|
13591
|
+
const view7 = { name: "questionAnswered", model: { answers, when: store.now(), zone, replied: lang } };
|
|
13592
|
+
await chat.conversation.update(cardId, viewMessage2(view7, words3));
|
|
13593
|
+
} else {
|
|
13594
|
+
await chat.say(words3);
|
|
13595
|
+
}
|
|
13596
|
+
} catch (error) {
|
|
13597
|
+
logger.warn("the question card could not be marked answered", { error: errorName(error) });
|
|
13598
|
+
}
|
|
13599
|
+
}
|
|
13600
|
+
|
|
13601
|
+
// packages/core/src/routing/turn-start.ts
|
|
13602
|
+
var MOBILE_PLATFORMS = ["ios", "ipados", "android"];
|
|
13603
|
+
function isMobile(clientPlatform) {
|
|
13604
|
+
return MOBILE_PLATFORMS.includes(clientPlatform.toLowerCase());
|
|
13605
|
+
}
|
|
13606
|
+
function logTurnStarted(logger, clientPlatform, group, route) {
|
|
13607
|
+
logger.info("turn started from Teams client", {
|
|
13608
|
+
platform: clientPlatform || "unknown",
|
|
13609
|
+
chat: group ? "group" : "1:1",
|
|
13610
|
+
route
|
|
13611
|
+
});
|
|
13612
|
+
}
|
|
13613
|
+
var TurnStarter = class {
|
|
13614
|
+
constructor(ctx) {
|
|
13615
|
+
this.ctx = ctx;
|
|
13616
|
+
}
|
|
13617
|
+
ctx;
|
|
13618
|
+
running = /* @__PURE__ */ new Set();
|
|
13619
|
+
/** The turns that run now (a test waits for them). */
|
|
13620
|
+
settled() {
|
|
13621
|
+
return Promise.all([...this.running]).then(() => void 0);
|
|
13622
|
+
}
|
|
13623
|
+
/** The turn that runs (its end is `done`), or undefined when none was started (the person was told why). */
|
|
13624
|
+
async start(request) {
|
|
13625
|
+
const { chat, review } = request;
|
|
13626
|
+
if (await this.answersQuestion(request)) return void 0;
|
|
13627
|
+
const key = review ? reviewTurnKey(chat.sender.id, chat.scope, review.itemId, review.pull) : chatTurnKey(chat.sender.id, chat.scope);
|
|
13628
|
+
const turn = new ActiveTurn(review !== void 0, review?.itemId ?? "");
|
|
13629
|
+
const admitted = review === void 0 && await this.admit(request, key, turn);
|
|
13630
|
+
if (admitted === void 0) return void 0;
|
|
13631
|
+
if (!admitted) {
|
|
13632
|
+
const queued = await this.queueBehind(key, turn, chat);
|
|
13633
|
+
if (queued === "refused") return void 0;
|
|
13634
|
+
if (queued === "waiting") return this.track(this.afterFront(request, key, turn));
|
|
13635
|
+
}
|
|
13636
|
+
return this.launch(request, key, turn);
|
|
13637
|
+
}
|
|
13638
|
+
/** A typed message the waiting question card of the chat's session took as its answer (`question-reply.ts`). */
|
|
13639
|
+
async answersQuestion(request) {
|
|
13640
|
+
if (!request.typed || request.review !== void 0 || (request.files?.length ?? 0) > 0) return false;
|
|
13641
|
+
const provider = this.providerOf(request);
|
|
13642
|
+
return answerWaitingQuestion(this.ctx, {
|
|
13643
|
+
chat: request.chat,
|
|
13644
|
+
text: request.text,
|
|
13645
|
+
credential: provider ?? (() => Promise.resolve(request.credential))
|
|
13646
|
+
});
|
|
13647
|
+
}
|
|
13648
|
+
track(task) {
|
|
13649
|
+
this.running.add(task);
|
|
13650
|
+
void task.finally(() => this.running.delete(task));
|
|
13651
|
+
return { done: task };
|
|
13652
|
+
}
|
|
13653
|
+
/** The turn goes to Claw now; its end is `done`. */
|
|
13654
|
+
async launch(request, key, turn) {
|
|
13655
|
+
turn.launched = true;
|
|
13656
|
+
let run;
|
|
13657
|
+
try {
|
|
13658
|
+
run = await this.runOf(request);
|
|
13659
|
+
} catch (error) {
|
|
13660
|
+
this.ctx.turns.drop(key, turn);
|
|
13661
|
+
throw error;
|
|
13662
|
+
}
|
|
13663
|
+
return this.track(this.guarded(request, run, key, turn));
|
|
13664
|
+
}
|
|
13665
|
+
/**
|
|
13666
|
+
* 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
|
|
13667
|
+
* 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
|
|
13668
|
+
* meanwhile (`/stop`, said there).
|
|
13669
|
+
*/
|
|
13670
|
+
async afterFront(request, key, turn) {
|
|
13671
|
+
for (; ; ) {
|
|
13672
|
+
if (turn.over) return;
|
|
13673
|
+
const turns = this.ctx.turns.of(key);
|
|
13674
|
+
const at = turns.indexOf(turn);
|
|
13675
|
+
if (at < 0) return;
|
|
13676
|
+
const blocking = turns.slice(0, at).find((front) => !front.stopped.isSet);
|
|
13677
|
+
if (blocking === void 0) break;
|
|
13678
|
+
await Promise.race([blocking.ended.whenSet(), blocking.stopped.whenSet(), turn.ended.whenSet()]);
|
|
13679
|
+
}
|
|
13680
|
+
let started;
|
|
13681
|
+
try {
|
|
13682
|
+
started = await this.launch(request, key, turn);
|
|
13683
|
+
} catch (error) {
|
|
13684
|
+
const ref = this.ctx.deps.random.hexId().slice(0, 8);
|
|
13685
|
+
this.ctx.deps.logger.error("a queued turn could not start", { ref, error: errorName(error) });
|
|
13686
|
+
await this.tell(request, `Something went wrong on my side (ref ${ref}). Please try again.`);
|
|
13687
|
+
return;
|
|
13688
|
+
}
|
|
13689
|
+
await started.done;
|
|
13690
|
+
}
|
|
13691
|
+
/** True when admitted as the chat's first turn; false when the chat has one (queue behind it); undefined: refused. */
|
|
13692
|
+
async admit(request, key, turn) {
|
|
13693
|
+
if (this.ctx.turns.has(key)) return false;
|
|
13694
|
+
const { chat } = request;
|
|
13695
|
+
return this.ctx.admission.lock.run(chat.sender.id, async () => {
|
|
13696
|
+
if (this.ctx.turns.has(key)) return false;
|
|
13697
|
+
if (await this.ctx.admission.overLimit(chat, request.messageId, "claw_turn", "", request.credential))
|
|
13698
|
+
return void 0;
|
|
13699
|
+
this.ctx.turns.claim(key, turn);
|
|
13700
|
+
return true;
|
|
13701
|
+
});
|
|
13702
|
+
}
|
|
13703
|
+
/**
|
|
13704
|
+
* Registers `turn` behind the chat's running one: "front" when the chat has none after all (it runs now), "waiting" when it
|
|
13705
|
+
* is queued (the person is told its place), "refused" (and told why) when it may not wait.
|
|
13706
|
+
*/
|
|
13707
|
+
async queueBehind(key, turn, chat) {
|
|
13708
|
+
const queued = this.ctx.turns.enqueue(key, turn);
|
|
13709
|
+
const said2 = routingText.in(chat.lang());
|
|
13710
|
+
if (queued === "still_working" || queued === "queue_full") {
|
|
13711
|
+
await chat.say(queued === "still_working" ? said2.stillWorking : said2.queueFull({ n: QUEUED_TURNS_MAX }));
|
|
13712
|
+
return "refused";
|
|
13713
|
+
}
|
|
13714
|
+
if (queued.front === turn) return "front";
|
|
13715
|
+
const stoppedInFront = this.ctx.turns.of(key).every((front) => front === turn || front.stopped.isSet);
|
|
13716
|
+
if (stoppedInFront) return "waiting";
|
|
13717
|
+
try {
|
|
13718
|
+
await chat.say(said2.queuedBehind({ n: queued.waiting }));
|
|
13719
|
+
} catch (error) {
|
|
13720
|
+
this.ctx.turns.drop(key, turn);
|
|
13721
|
+
throw error;
|
|
13722
|
+
}
|
|
13723
|
+
return "waiting";
|
|
13724
|
+
}
|
|
13725
|
+
async sessionOf(request) {
|
|
13726
|
+
const { chat, review } = request;
|
|
13727
|
+
const { store, config } = this.ctx.deps;
|
|
13728
|
+
if (review !== void 0) return review.itemSessionId;
|
|
13729
|
+
const sessionId = store.sessions.getSession(chat.sender.id, chat.scope);
|
|
13730
|
+
if (sessionId === null) return null;
|
|
13731
|
+
if (Boolean(store.sessions.getSessionPlugin(chat.sender.id, chat.scope)) !== Boolean(config.pluginId)) {
|
|
13732
|
+
await chat.say("Starting a new session so I can use the Primus platform tools.");
|
|
13733
|
+
return null;
|
|
13734
|
+
}
|
|
13735
|
+
if (chat.scope && !store.sessions.getSessionShared(chat.sender.id, chat.scope)) return null;
|
|
13736
|
+
return sessionId;
|
|
13737
|
+
}
|
|
13738
|
+
providerOf(request) {
|
|
13739
|
+
const usesExchange = request.review !== void 0 || this.ctx.credentials.onExchange;
|
|
13740
|
+
if (!this.ctx.deps.exchange.enabled || !usesExchange) return void 0;
|
|
13741
|
+
return () => this.ctx.credentials.fresh(request.chat.sender.id, request.credential);
|
|
13742
|
+
}
|
|
13743
|
+
ownThumbs(request) {
|
|
13744
|
+
return isMobile(request.clientPlatform) && request.review === void 0 && this.ctx.deps.config.ownThumbsMobile;
|
|
13745
|
+
}
|
|
13746
|
+
/** What the run reads of the chat's session: its id, the model it has, the repository a new one is bound to. */
|
|
13747
|
+
async sessionPart(request) {
|
|
13748
|
+
const { chat, review } = request;
|
|
13749
|
+
const { store } = this.ctx.deps;
|
|
13750
|
+
const sessionId = await this.sessionOf(request);
|
|
13751
|
+
const chosen = sessionId === null && !chat.scope ? store.repoHistory.getPendingRepo(chat.sender.id) : null;
|
|
13752
|
+
return {
|
|
13753
|
+
sessionId,
|
|
13754
|
+
sessionModel: sessionId === null || review ? "" : store.sessions.getSessionModel(chat.sender.id, chat.scope),
|
|
13755
|
+
repo: chosen === null ? null : { url: `${REPO_HOST}/${chosen.repo}`, branch: chosen.branch }
|
|
13756
|
+
};
|
|
13757
|
+
}
|
|
13758
|
+
async runOf(request) {
|
|
13759
|
+
const { chat, review } = request;
|
|
13760
|
+
const { store, config, logger } = this.ctx.deps;
|
|
13761
|
+
const session = await this.sessionPart(request);
|
|
13762
|
+
const group = chat.scope !== "";
|
|
13763
|
+
logTurnStarted(logger, request.clientPlatform, group, review ? "review" : "legacy");
|
|
13764
|
+
return {
|
|
13765
|
+
...session,
|
|
13766
|
+
conversation: chat.conversation,
|
|
13767
|
+
credential: request.credential,
|
|
13768
|
+
provider: this.providerOf(request),
|
|
13769
|
+
text: request.text,
|
|
13770
|
+
messageId: request.messageId,
|
|
13771
|
+
group,
|
|
13772
|
+
userId: chat.sender.id,
|
|
13773
|
+
ownerName: chat.sender.name,
|
|
13774
|
+
model: review ? review.model || config.reviewModel || config.model : config.model,
|
|
13775
|
+
fallbackModel: config.fallbackModel,
|
|
13776
|
+
showChanges: !group && review === void 0,
|
|
13777
|
+
quote: request.quote ?? "",
|
|
13778
|
+
sent: request.sent ?? "",
|
|
13779
|
+
files: request.files ?? [],
|
|
13780
|
+
lang: review?.lang ?? chat.lang(),
|
|
13781
|
+
ownThumbs: this.ownThumbs(request),
|
|
13782
|
+
notifies: this.ctx.deps.subscriptions !== void 0 && store.notifySubs.getNotifySub(chat.sender.id) !== null,
|
|
13783
|
+
review: review?.fields
|
|
13784
|
+
};
|
|
13785
|
+
}
|
|
13786
|
+
hooks(request, run, turn, turnId) {
|
|
13787
|
+
const { review, chat } = request;
|
|
13788
|
+
const store = this.ctx.deps.store;
|
|
13789
|
+
return {
|
|
13790
|
+
onSession: (sessionId, sessionModel) => {
|
|
13791
|
+
if (review) review.onSession(sessionId);
|
|
13792
|
+
else this.recordSession(chat, sessionId, run, sessionModel);
|
|
13793
|
+
},
|
|
13794
|
+
onRunning: () => {
|
|
13795
|
+
turn.running.set();
|
|
13796
|
+
},
|
|
13797
|
+
onTurn: (sessionId, what, messageId) => {
|
|
13798
|
+
if (what === "started") store.turns.turnStarted(sessionId, { prompt: run.text, messageId, turnId });
|
|
13799
|
+
else if (what === "delivered") store.turns.turnDelivered(sessionId, { messageId, turnId });
|
|
13800
|
+
else store.turns.turnNotDelivered(sessionId, turnId);
|
|
13801
|
+
},
|
|
13802
|
+
stopRequested: () => turn.stopRequested,
|
|
13803
|
+
toolDenied: () => turn.toolDenied,
|
|
13804
|
+
stopConfirmed: () => turn.stopConfirmed,
|
|
13805
|
+
reportView: () => this.ctx.deps.reportView(chat.sender.id, chat.scope)
|
|
13806
|
+
};
|
|
13807
|
+
}
|
|
13808
|
+
async guarded(request, run, key, turn) {
|
|
13809
|
+
const turnId = this.ctx.deps.random.hexId();
|
|
13810
|
+
try {
|
|
13811
|
+
const outcome = await this.ctx.deps.turns.run(run, this.hooks(request, run, turn, turnId));
|
|
13812
|
+
if (request.review) await request.review.after(outcome);
|
|
13813
|
+
} catch (error) {
|
|
13814
|
+
const ref = this.ctx.deps.random.hexId().slice(0, 8);
|
|
13815
|
+
this.ctx.deps.logger.error("turn failed", { ref, error: errorName(error) });
|
|
13816
|
+
await this.tell(request, `Something went wrong on my side (ref ${ref}). Please try again.`);
|
|
13817
|
+
} finally {
|
|
13818
|
+
this.ctx.turns.drop(key, turn);
|
|
13819
|
+
}
|
|
13820
|
+
}
|
|
13821
|
+
async tell(request, text8) {
|
|
13822
|
+
try {
|
|
13823
|
+
await request.chat.say(text8);
|
|
13824
|
+
const report = this.ctx.deps.reportView(request.chat.sender.id, request.chat.scope);
|
|
13825
|
+
if (report !== void 0) await request.chat.send(report);
|
|
13826
|
+
} catch (error) {
|
|
13827
|
+
this.ctx.deps.logger.warn("a failed turn could not be said", { error: errorName(error) });
|
|
13828
|
+
}
|
|
13829
|
+
}
|
|
13830
|
+
/**
|
|
13831
|
+
* The turn has a session: it is the chat's session, and a 1:1 chat remembers it for /sessions, with what a session
|
|
13832
|
+
* this turn created was started with and the repository it is bound to (which uses up the `/repo` choice).
|
|
13833
|
+
*/
|
|
13834
|
+
recordSession(chat, sessionId, run, sessionModel) {
|
|
13835
|
+
const { store, config } = this.ctx.deps;
|
|
13836
|
+
const userId = chat.sender.id;
|
|
13837
|
+
store.sessions.setSession(
|
|
13838
|
+
userId,
|
|
13839
|
+
{ sessionId, pluginId: config.pluginId, model: sessionModel, shared: chat.scope !== "" },
|
|
13840
|
+
chat.scope
|
|
13841
|
+
);
|
|
13842
|
+
if (chat.scope) return;
|
|
13843
|
+
const created = run.sessionId !== sessionId;
|
|
13844
|
+
if (created && run.sessionId) store.repoHistory.forgetSession(userId, run.sessionId);
|
|
13845
|
+
const url = run.repo?.url ?? "";
|
|
13846
|
+
const bound = created && run.repo ? url.startsWith(`${REPO_HOST}/`) ? url.slice(REPO_HOST.length + 1) : url : "";
|
|
13847
|
+
store.repoHistory.noteSession(userId, sessionId, {
|
|
13848
|
+
title: created ? cpHead(squashSpaces(run.text), 50) : "",
|
|
13849
|
+
repo: bound,
|
|
13850
|
+
branch: bound && run.repo ? run.repo.branch : ""
|
|
13851
|
+
});
|
|
13852
|
+
if (created && run.repo) store.repoHistory.clearPendingRepo(userId);
|
|
13853
|
+
}
|
|
13854
|
+
};
|
|
13855
|
+
|
|
13856
|
+
// packages/core/src/routing/personal-text.ts
|
|
13857
|
+
var ELSEWHERE = "Files are not passed on from this chat while it talks to an agent; only your words were sent.";
|
|
13858
|
+
var PersonalText = class {
|
|
13859
|
+
constructor(ctx, commands, starter) {
|
|
13860
|
+
this.ctx = ctx;
|
|
13861
|
+
this.commands = commands;
|
|
13862
|
+
this.starter = starter;
|
|
13863
|
+
}
|
|
13864
|
+
ctx;
|
|
13865
|
+
commands;
|
|
13866
|
+
starter;
|
|
13867
|
+
/**
|
|
13868
|
+
* What the person wrote, with a word of the commands menu read as its command and a sentence help teaches read as the
|
|
13869
|
+
* command it means; a sentence for what this chat cannot do right now stays words.
|
|
13870
|
+
*/
|
|
13871
|
+
textOf(message, group = false) {
|
|
13872
|
+
const { github, assistant, remote, config } = this.ctx.deps;
|
|
13873
|
+
const userId = group ? void 0 : message.sender.id;
|
|
13874
|
+
const on = (test) => userId !== void 0 && test(userId);
|
|
13875
|
+
const attached = on((id) => remote.attached(id));
|
|
13876
|
+
const off = /* @__PURE__ */ new Set();
|
|
13877
|
+
const rules = [
|
|
13878
|
+
["github", github !== void 0],
|
|
13879
|
+
["board", on((id) => assistant.on(id))],
|
|
13880
|
+
["brief", config.briefEnabled],
|
|
13881
|
+
["brief now", on((id) => assistant.on(id))],
|
|
13882
|
+
["agents", remote.enabled],
|
|
13883
|
+
["agent", remote.enabled],
|
|
13884
|
+
["agent detach", attached],
|
|
13885
|
+
["stop it", attached],
|
|
13886
|
+
// ("停掉它" stops the attached agent; with the assistant it is the assistant's to read)
|
|
13887
|
+
["new", !(attached && on((id) => assistant.on(id)))]
|
|
13888
|
+
// (in an attached chat: a new agent session, the assistant's)
|
|
13889
|
+
];
|
|
13890
|
+
for (const [word2, enabled] of rules) if (!enabled) off.add(word2);
|
|
13891
|
+
if (remote.enabled && on((id) => assistant.on(id)) && menuCommand(message.text, group) === "/agent sessions" && !/agent/iu.test(message.text))
|
|
13892
|
+
return "/sessions";
|
|
13893
|
+
return naturalCommand(menuCommand(message.text, group), group, off);
|
|
13894
|
+
}
|
|
13895
|
+
async onText(message, chat, key) {
|
|
13896
|
+
const text8 = this.textOf(message);
|
|
13897
|
+
if (parseCommand(text8) === "unlink" && !message.text.startsWith("/")) {
|
|
13898
|
+
await chat.say(routingText.in(chat.lang()).unlinkAsk);
|
|
13899
|
+
return;
|
|
13900
|
+
}
|
|
13901
|
+
if (await this.commands.run(text8, chat, key, message)) return;
|
|
13902
|
+
if (!this.talksToAgent(message))
|
|
13903
|
+
await this.offerGithub(message, chat, key, text8, (m, fresh) => this.sentOn(m, chat, fresh));
|
|
13904
|
+
await this.sentOn(message, chat, key);
|
|
13905
|
+
}
|
|
13906
|
+
/**
|
|
13907
|
+
* Whether this 1:1 message goes to the remote session the chat is attached to: only words the person typed, and not
|
|
13908
|
+
* those that start by naming the bot. A chat whose session card is the window keeps its chat box for the bot: only a
|
|
13909
|
+
* reply that quotes the card goes to the session.
|
|
13910
|
+
*/
|
|
13911
|
+
talksToAgent(message) {
|
|
13912
|
+
const { remote } = this.ctx.deps;
|
|
13913
|
+
const userId = message.sender.id;
|
|
13914
|
+
if (!remote.attached(userId) || message.provenance !== "typed" || message.namesBotFirst) return false;
|
|
13915
|
+
if (remote.windowMode(userId) === "card") return remote.quotesWindow(userId, message);
|
|
13916
|
+
return true;
|
|
13917
|
+
}
|
|
13918
|
+
/** The files that go to Claw with a message; the person is told about the ones that do not, and they are counted. */
|
|
13919
|
+
async attachments(message, chat, elsewhere = false) {
|
|
13920
|
+
const { messageText, metrics } = this.ctx.deps;
|
|
13921
|
+
const unreadable = messageText.unreadable(message);
|
|
13922
|
+
if (unreadable !== void 0) {
|
|
13923
|
+
metrics.filesDropped("unreadable", unreadable.count);
|
|
13924
|
+
await chat.say(unreadable.line);
|
|
13925
|
+
}
|
|
13926
|
+
if (message.files.length > 0 && elsewhere) {
|
|
13927
|
+
metrics.filesDropped("remote_agent", message.files.length);
|
|
13928
|
+
await chat.say(ELSEWHERE);
|
|
13929
|
+
return [];
|
|
13930
|
+
}
|
|
13931
|
+
return message.files;
|
|
13932
|
+
}
|
|
13933
|
+
/** What the chat is attached to, for the assistant (only where remote agents are on). */
|
|
13934
|
+
async attached(userId, key) {
|
|
13935
|
+
const { remote } = this.ctx.deps;
|
|
13936
|
+
return remote.enabled ? { attached: await remote.chatContext(userId, key) } : {};
|
|
13937
|
+
}
|
|
13938
|
+
/** A 1:1 message that is no command, as it goes on to the attached agent, the assistant or Claw. */
|
|
13939
|
+
async sentOn(message, chat, key) {
|
|
13940
|
+
const { messageText, assistant } = this.ctx.deps;
|
|
13941
|
+
const userId = chat.sender.id;
|
|
13942
|
+
const text8 = this.textOf(message);
|
|
13943
|
+
const toAgent = this.talksToAgent(message);
|
|
13944
|
+
const quote = toAgent && this.ctx.deps.remote.quotesWindow(userId, message) ? "" : messageText.quoteBlock(message, false);
|
|
13945
|
+
const sent = messageText.withLinks(text8, message);
|
|
13946
|
+
const prompt = messageText.withQuote(sent, quote);
|
|
13947
|
+
const files = await this.attachments(message, chat, toAgent);
|
|
13948
|
+
const control = this.sessionControl(userId, prompt, toAgent, quote, files, text8);
|
|
13949
|
+
if (control && assistant.understands(userId)) {
|
|
13950
|
+
await this.understand(message, chat, key);
|
|
13951
|
+
} else if (prompt && toAgent && !control) {
|
|
13952
|
+
await this.remotePrompt(message, chat, key, prompt);
|
|
13953
|
+
} else if (prompt || files.length > 0) {
|
|
13954
|
+
await this.toAssistantOrTurn(message, chat, key, { text: text8, quote, sent, prompt, files });
|
|
13955
|
+
}
|
|
13956
|
+
}
|
|
13957
|
+
/**
|
|
13958
|
+
* A message to an attached chat that is about the sessions themselves (switch, open a new one), with the assistant on to
|
|
13959
|
+
* read it: the words alone (a quote or a file goes to the agent with them).
|
|
13960
|
+
*/
|
|
13961
|
+
sessionControl(userId, prompt, toAgent, quote, files, text8) {
|
|
13962
|
+
if (!prompt || !toAgent || quote || files.length > 0) return false;
|
|
13963
|
+
return this.ctx.deps.assistant.on(userId) && sessionControl(text8);
|
|
13964
|
+
}
|
|
13965
|
+
/** A message the assistant takes when it is on, else a turn of Claw's. */
|
|
13966
|
+
async toAssistantOrTurn(message, chat, key, said2) {
|
|
13967
|
+
const { assistant } = this.ctx.deps;
|
|
13968
|
+
const userId = chat.sender.id;
|
|
13969
|
+
if (assistant.on(userId) && await this.ctx.admission.assistantOverLimit(chat, message.id, key)) return;
|
|
13970
|
+
const routed = await assistant.route({
|
|
13971
|
+
userId,
|
|
13972
|
+
credential: key,
|
|
13973
|
+
prompt: said2.prompt || assistant.filesOnly(chat.lang()),
|
|
13974
|
+
conversation: chat.conversation,
|
|
13975
|
+
files: said2.files,
|
|
13976
|
+
...await this.attached(userId, key)
|
|
13977
|
+
});
|
|
13978
|
+
if (routed) {
|
|
13979
|
+
logTurnStarted(this.ctx.deps.logger, message.clientPlatform, false, "assistant");
|
|
13980
|
+
return;
|
|
13981
|
+
}
|
|
13982
|
+
this.ctx.admission.dropLastHandoff(userId);
|
|
13983
|
+
await this.starter.start({
|
|
13984
|
+
chat,
|
|
13985
|
+
credential: key,
|
|
13986
|
+
text: said2.text,
|
|
13987
|
+
messageId: message.id,
|
|
13988
|
+
clientPlatform: message.clientPlatform,
|
|
13989
|
+
quote: said2.quote,
|
|
13990
|
+
sent: said2.sent,
|
|
13991
|
+
files: said2.files,
|
|
13992
|
+
typed: true
|
|
13993
|
+
});
|
|
13994
|
+
}
|
|
13995
|
+
/** A prompt for the attached agent, within the task limit (the slot is held until the remote turn counts as running). */
|
|
13996
|
+
async remotePrompt(message, chat, key, prompt) {
|
|
13997
|
+
const { remote } = this.ctx.deps;
|
|
13998
|
+
const request = {
|
|
13999
|
+
message,
|
|
14000
|
+
chat: chat.sender,
|
|
14001
|
+
credential: key,
|
|
14002
|
+
prompt,
|
|
14003
|
+
conversation: chat.conversation,
|
|
14004
|
+
provider: this.ctx.credentials.provider(chat.sender.id, key)
|
|
14005
|
+
};
|
|
14006
|
+
if (remote.running(chat.sender.id)) {
|
|
14007
|
+
await remote.prompt(request);
|
|
14008
|
+
return;
|
|
14009
|
+
}
|
|
14010
|
+
if (!await this.ctx.admission.admitRemote(chat, message.id, key)) return;
|
|
14011
|
+
try {
|
|
14012
|
+
await remote.prompt(request);
|
|
14013
|
+
} finally {
|
|
14014
|
+
this.ctx.admission.letGo(chat.sender.id);
|
|
14015
|
+
}
|
|
13432
14016
|
}
|
|
13433
14017
|
/**
|
|
13434
14018
|
* Beside a question about the person's own GitHub while Claw has no token of theirs: the connect card first; once they
|
|
@@ -13493,6 +14077,7 @@ var PersonalText = class {
|
|
|
13493
14077
|
});
|
|
13494
14078
|
const held = assistant.reachable(userId);
|
|
13495
14079
|
if (held && await this.ctx.admission.assistantOverLimit(chat, message.id, key)) return;
|
|
14080
|
+
if (held) logTurnStarted(logger, message.clientPlatform, false, "assistant");
|
|
13496
14081
|
store.assistantMessages.noteAssistantMessage(
|
|
13497
14082
|
channelMessageId,
|
|
13498
14083
|
userId,
|
|
@@ -13630,439 +14215,200 @@ var GroupRouter = class {
|
|
|
13630
14215
|
async withKey(message, chat, key, words3) {
|
|
13631
14216
|
const command = parseCommand(words3.text);
|
|
13632
14217
|
if (command === "new") await this.sessions.new(chat, chat.scope);
|
|
13633
|
-
else if (command === "stop") await this.sessions.stop(chat, key, chat.scope);
|
|
13634
|
-
else if (command === "whoami") await this.whoami(chat, key);
|
|
13635
|
-
else {
|
|
13636
|
-
const files = await this.text.attachments(message, chat);
|
|
13637
|
-
if (!words3.sent && !words3.quote && files.length === 0) return;
|
|
13638
|
-
await this.starter.start({
|
|
13639
|
-
chat,
|
|
13640
|
-
credential: key,
|
|
13641
|
-
text: words3.text,
|
|
13642
|
-
messageId: message.id,
|
|
13643
|
-
clientPlatform: message.clientPlatform,
|
|
13644
|
-
quote: this.withHistory(message, words3.quote),
|
|
13645
|
-
sent: words3.sent,
|
|
13646
|
-
files
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
}
|
|
13650
|
-
/** The chat's recent messages, as untrusted background, before what the person quoted (either may be ""). */
|
|
13651
|
-
withHistory(message, quote) {
|
|
13652
|
-
const history = this.ctx.deps.groupHistory.background(message.conversation.id);
|
|
13653
|
-
return history && quote ? `${history}
|
|
13654
|
-
|
|
13655
|
-
${quote}` : history || quote;
|
|
13656
|
-
}
|
|
13657
|
-
/** Which account, in a group: never the sender's email. */
|
|
13658
|
-
async whoami(chat, key) {
|
|
13659
|
-
const said2 = groupText.in(chat.lang());
|
|
13660
|
-
const who = await this.ctx.owners.whoLine(key, false);
|
|
13661
|
-
if (chat.zh()) {
|
|
13662
|
-
const name = who === void 0 ? "" : (await this.ctx.owners.whoami(key))?.[0] ?? "";
|
|
13663
|
-
await chat.say(name ? said2.whoami({ name: chat.sender.name, who: name }) : said2.whoamiUnknown);
|
|
13664
|
-
return;
|
|
13665
|
-
}
|
|
13666
|
-
await chat.say(who === void 0 ? said2.whoamiUnknown : said2.whoami({ name: chat.sender.name, who }));
|
|
13667
|
-
}
|
|
13668
|
-
/**
|
|
13669
|
-
* "停 825", from a person the assistant understands: their reviews of those pull requests are stopped in Claw at once
|
|
13670
|
-
* (not said to the assistant, where it could wait while the merge happens). Placed by the assembly among the group's
|
|
13671
|
-
* interceptors (after feedback, before review requests).
|
|
13672
|
-
*/
|
|
13673
|
-
reviewStopInterceptor() {
|
|
13674
|
-
return {
|
|
13675
|
-
name: "review_stop",
|
|
13676
|
-
intercept: async (route) => {
|
|
13677
|
-
const { assistant, reviewStop: reviewStop2 } = this.ctx.deps;
|
|
13678
|
-
if (reviewStop2 === void 0 || !assistant.understands(route.sender.id)) return false;
|
|
13679
|
-
const pulls = reviewStop2(route.text);
|
|
13680
|
-
if (pulls.length === 0) return false;
|
|
13681
|
-
await this.stopReviews(this.ctx.chatOf(route.message, route.scope), pulls, route.text);
|
|
13682
|
-
return true;
|
|
13683
|
-
}
|
|
13684
|
-
};
|
|
13685
|
-
}
|
|
13686
|
-
async stopReviews(chat, pulls, text8) {
|
|
13687
|
-
const zh = hasCjk2(text8);
|
|
13688
|
-
const said2 = groupText.in(zh ? "zh" : "en");
|
|
13689
|
-
const key = await this.ctx.credentials.credential(chat, false);
|
|
13690
|
-
let items;
|
|
13691
|
-
try {
|
|
13692
|
-
items = key === void 0 ? void 0 : await this.ctx.deps.claw.workItems.list(key, "in_progress");
|
|
13693
|
-
} catch {
|
|
13694
|
-
items = void 0;
|
|
13695
|
-
}
|
|
13696
|
-
if (key === void 0 || items === void 0) {
|
|
13697
|
-
await chat.say(said2.stopUnreachable);
|
|
13698
|
-
return;
|
|
13699
|
-
}
|
|
13700
|
-
const lines = [];
|
|
13701
|
-
for (const [slug2, number] of pulls) {
|
|
13702
|
-
const found = items.filter(
|
|
13703
|
-
(i) => i.kind === "review" && i.pull !== null && i.pull.number === number && (slug2 === "" || slug2 === pyLower(`${i.pull.owner}/${i.pull.repo}`))
|
|
13704
|
-
);
|
|
13705
|
-
const [only2] = found;
|
|
13706
|
-
if (only2 === void 0) lines.push(said2.stopNone({ number }));
|
|
13707
|
-
else if (found.length > 1) lines.push(said2.stopMany({ number }));
|
|
13708
|
-
else lines.push(await this.stopReview(key, only2, zh));
|
|
13709
|
-
}
|
|
13710
|
-
await chat.say(lines.join("\n"));
|
|
13711
|
-
}
|
|
13712
|
-
async stopReview(key, item, zh) {
|
|
13713
|
-
const pull2 = item.pull ?? { owner: "", repo: "", number: 0 };
|
|
13714
|
-
const short = `${pull2.owner}/${pull2.repo}#${String(pull2.number)}`;
|
|
13715
|
-
let error;
|
|
13716
|
-
try {
|
|
13717
|
-
const decision = await this.ctx.deps.claw.workItems.stop(key, item.itemId);
|
|
13718
|
-
error = decision.ok ? "" : decision.error;
|
|
13719
|
-
if (decision.ok) this.ctx.itemStopped(item.itemId);
|
|
13720
|
-
} catch (failure) {
|
|
13721
|
-
if (failure instanceof ClawUnreachable)
|
|
13722
|
-
return zh ? `${short}\uFF1A\u6CA1\u8FDE\u4E0A PrimusClaw\uFF0C\u8BF7\u518D\u8BF4\u4E00\u6B21\u3002` : `${short}: PrimusClaw did not answer; please say it again.`;
|
|
13723
|
-
if (!(failure instanceof ClawError)) throw failure;
|
|
13724
|
-
error = failure.status === 409 && isErrorWord(failure.code) ? failure.code : `error ${String(failure.status)}`;
|
|
13725
|
-
}
|
|
13726
|
-
const said2 = (Object.hasOwn(STOP_SAID, error) ? STOP_SAID[error] : void 0) ?? [
|
|
13727
|
-
`\u6CA1\u80FD\u505C\u4E0B\uFF08${error}\uFF09\u3002`,
|
|
13728
|
-
`it could not be stopped (${error}).`
|
|
13729
|
-
];
|
|
13730
|
-
return zh ? `${short}\uFF1A${said2[0]}` : `${short}: ${said2[1]}`;
|
|
13731
|
-
}
|
|
13732
|
-
};
|
|
13733
|
-
|
|
13734
|
-
// packages/core/src/routing/link-commands.ts
|
|
13735
|
-
var LinkCommands = class {
|
|
13736
|
-
constructor(ctx, resume2) {
|
|
13737
|
-
this.ctx = ctx;
|
|
13738
|
-
this.resume = resume2;
|
|
13739
|
-
}
|
|
13740
|
-
ctx;
|
|
13741
|
-
resume;
|
|
13742
|
-
async link(chat, code3, messageId) {
|
|
13743
|
-
const { store } = this.ctx.deps;
|
|
13744
|
-
const outcome = store.links.redeemPending(chat.sender.id, chat.sender.name, code3 ?? "", messageId || void 0);
|
|
13745
|
-
const said2 = linkText.in("en");
|
|
13746
|
-
if (outcome === "ok") await this.linked(chat);
|
|
13747
|
-
else if (outcome === "limited") {
|
|
13748
|
-
const minutes = Math.max(1, Math.ceil(store.links.linkWindowSeconds / 60));
|
|
13749
|
-
await chat.say(said2.tooManyWrong({ minutes }));
|
|
13750
|
-
} else if (outcome === "global_limit") await chat.say(said2.linkingPaused);
|
|
13751
|
-
else await chat.say(said2.badCode);
|
|
13752
|
-
}
|
|
13753
|
-
async linked(chat) {
|
|
13754
|
-
const { store, subscriptions } = this.ctx.deps;
|
|
13755
|
-
const userId = chat.sender.id;
|
|
13756
|
-
const key = store.links.getKey(userId) ?? "";
|
|
13757
|
-
const clawUserId = key ? await this.ctx.owners.userIdOf(key) : void 0;
|
|
13758
|
-
if (clawUserId !== void 0) store.links.setClawUserId(userId, clawUserId);
|
|
13759
|
-
if (subscriptions !== void 0) await subscriptions.ensure(userId);
|
|
13760
|
-
const pending = store.onboarding.takePendingRequest(userId);
|
|
13761
|
-
const zh = chat.zh();
|
|
13762
|
-
const said2 = linkText.in(zh ? "zh" : "en");
|
|
13763
|
-
let who;
|
|
13764
|
-
if (zh) {
|
|
13765
|
-
const owner = key ? await this.ctx.owners.whoami(key) : void 0;
|
|
13766
|
-
who = owner === void 0 ? "" : `\u5230 SaFE \u7528\u6237 ${owner[0]}` + (owner[1] ? `\uFF08${owner[1]}\uFF09` : "");
|
|
13767
|
-
} else {
|
|
13768
|
-
who = await this.ctx.owners.whoLine(key) ?? "";
|
|
13769
|
-
}
|
|
13770
|
-
await chat.say(said2.linkedAs({ who }) + (pending === null ? said2.getStarted : said2.runningFromGroup));
|
|
13771
|
-
if (pending !== null) await this.resume(chat, pending, true);
|
|
13772
|
-
}
|
|
13773
|
-
async unlink(chat) {
|
|
13774
|
-
const { store, remote, assistant, subscriptions, keyRevoker } = this.ctx.deps;
|
|
13775
|
-
const userId = chat.sender.id;
|
|
13776
|
-
remote.forgetPerson(userId);
|
|
13777
|
-
assistant.forget(userId);
|
|
13778
|
-
const stored2 = store.links.getKey(userId);
|
|
13779
|
-
if (stored2 === null) {
|
|
13780
|
-
store.sessions.clearSession(userId);
|
|
13781
|
-
await chat.say(unlinkText.in(chat.lang()).bindingOnly);
|
|
13782
|
-
return;
|
|
13783
|
-
}
|
|
13784
|
-
if (subscriptions !== void 0) await subscriptions.remove(userId, stored2);
|
|
13785
|
-
const revoked = await keyRevoker.revoke(stored2);
|
|
13786
|
-
store.links.unlink(userId);
|
|
13787
|
-
const stillBound = await this.stillBound(userId);
|
|
13788
|
-
const said2 = unlinkText.in(chat.lang());
|
|
13789
|
-
const tail = (revoked ? said2.deleted : said2.notDeleted) + (stillBound ? said2.stillBound : "");
|
|
13790
|
-
await chat.say(said2.unlinked({ tail }));
|
|
13791
|
-
}
|
|
13792
|
-
/** A chat that runs on the exchange stays connected through the person's binding, which only they can remove. */
|
|
13793
|
-
async stillBound(userId) {
|
|
13794
|
-
if (!this.ctx.credentials.onExchange) return false;
|
|
13795
|
-
this.ctx.deps.exchange.invalidate(userId);
|
|
13796
|
-
return (await this.ctx.deps.exchange.tokenFor(userId)).ok;
|
|
13797
|
-
}
|
|
13798
|
-
/**
|
|
13799
|
-
* The person pressed the button that says they confirmed: the binding is looked for now. The worker cannot make it,
|
|
13800
|
-
* only the person can; not there yet, the card comes back with the link that is still open (or a new one).
|
|
13801
|
-
*/
|
|
13802
|
-
async confirmBinding(action, chat) {
|
|
13803
|
-
const { exchange, store, logger } = this.ctx.deps;
|
|
13804
|
-
const said2 = linkText.in("en");
|
|
13805
|
-
exchange.invalidate(chat.sender.id);
|
|
13806
|
-
const token = await exchange.tokenFor(chat.sender.id);
|
|
13807
|
-
if (!token.ok && token.error.kind === "not_bound") {
|
|
13808
|
-
const notYet = linkText.in(chat.lang());
|
|
13809
|
-
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));
|
|
14218
|
+
else if (command === "stop") await this.sessions.stop(chat, key, chat.scope);
|
|
14219
|
+
else if (command === "whoami") await this.whoami(chat, key);
|
|
14220
|
+
else {
|
|
14221
|
+
const files = await this.text.attachments(message, chat);
|
|
14222
|
+
if (!words3.sent && !words3.quote && files.length === 0) return;
|
|
14223
|
+
await this.starter.start({
|
|
14224
|
+
chat,
|
|
14225
|
+
credential: key,
|
|
14226
|
+
text: words3.text,
|
|
14227
|
+
messageId: message.id,
|
|
14228
|
+
clientPlatform: message.clientPlatform,
|
|
14229
|
+
quote: this.withHistory(message, words3.quote),
|
|
14230
|
+
sent: words3.sent,
|
|
14231
|
+
files,
|
|
14232
|
+
typed: true
|
|
14233
|
+
});
|
|
13855
14234
|
}
|
|
13856
|
-
return this.launch(request, key, turn);
|
|
13857
14235
|
}
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
return
|
|
14236
|
+
/** The chat's recent messages, as untrusted background, before what the person quoted (either may be ""). */
|
|
14237
|
+
withHistory(message, quote) {
|
|
14238
|
+
const history = this.ctx.deps.groupHistory.background(message.conversation.id);
|
|
14239
|
+
return history && quote ? `${history}
|
|
14240
|
+
|
|
14241
|
+
${quote}` : history || quote;
|
|
13862
14242
|
}
|
|
13863
|
-
/**
|
|
13864
|
-
async
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
throw error;
|
|
14243
|
+
/** Which account, in a group: never the sender's email. */
|
|
14244
|
+
async whoami(chat, key) {
|
|
14245
|
+
const said2 = groupText.in(chat.lang());
|
|
14246
|
+
const who = await this.ctx.owners.whoLine(key, false);
|
|
14247
|
+
if (chat.zh()) {
|
|
14248
|
+
const name = who === void 0 ? "" : (await this.ctx.owners.whoami(key))?.[0] ?? "";
|
|
14249
|
+
await chat.say(name ? said2.whoami({ name: chat.sender.name, who: name }) : said2.whoamiUnknown);
|
|
14250
|
+
return;
|
|
13872
14251
|
}
|
|
13873
|
-
|
|
14252
|
+
await chat.say(who === void 0 ? said2.whoamiUnknown : said2.whoami({ name: chat.sender.name, who }));
|
|
13874
14253
|
}
|
|
13875
14254
|
/**
|
|
13876
|
-
*
|
|
13877
|
-
*
|
|
13878
|
-
*
|
|
14255
|
+
* "停 825", from a person the assistant understands: their reviews of those pull requests are stopped in Claw at once
|
|
14256
|
+
* (not said to the assistant, where it could wait while the merge happens). Placed by the assembly among the group's
|
|
14257
|
+
* interceptors (after feedback, before review requests).
|
|
13879
14258
|
*/
|
|
13880
|
-
|
|
13881
|
-
|
|
13882
|
-
|
|
13883
|
-
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
|
|
13890
|
-
|
|
14259
|
+
reviewStopInterceptor() {
|
|
14260
|
+
return {
|
|
14261
|
+
name: "review_stop",
|
|
14262
|
+
intercept: async (route) => {
|
|
14263
|
+
const { assistant, reviewStop: reviewStop2 } = this.ctx.deps;
|
|
14264
|
+
if (reviewStop2 === void 0 || !assistant.understands(route.sender.id)) return false;
|
|
14265
|
+
const pulls = reviewStop2(route.text);
|
|
14266
|
+
if (pulls.length === 0) return false;
|
|
14267
|
+
await this.stopReviews(this.ctx.chatOf(route.message, route.scope), pulls, route.text);
|
|
14268
|
+
return true;
|
|
14269
|
+
}
|
|
14270
|
+
};
|
|
14271
|
+
}
|
|
14272
|
+
async stopReviews(chat, pulls, text8) {
|
|
14273
|
+
const zh = hasCjk2(text8);
|
|
14274
|
+
const said2 = groupText.in(zh ? "zh" : "en");
|
|
14275
|
+
const key = await this.ctx.credentials.credential(chat, false);
|
|
14276
|
+
let items;
|
|
13891
14277
|
try {
|
|
13892
|
-
|
|
13893
|
-
} catch
|
|
13894
|
-
|
|
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;
|
|
14278
|
+
items = key === void 0 ? void 0 : await this.ctx.deps.claw.workItems.list(key, "in_progress");
|
|
14279
|
+
} catch {
|
|
14280
|
+
items = void 0;
|
|
13898
14281
|
}
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
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";
|
|
14282
|
+
if (key === void 0 || items === void 0) {
|
|
14283
|
+
await chat.say(said2.stopUnreachable);
|
|
14284
|
+
return;
|
|
13923
14285
|
}
|
|
13924
|
-
|
|
13925
|
-
const
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
14286
|
+
const lines = [];
|
|
14287
|
+
for (const [slug2, number] of pulls) {
|
|
14288
|
+
const found = items.filter(
|
|
14289
|
+
(i) => i.kind === "review" && i.pull !== null && i.pull.number === number && (slug2 === "" || slug2 === pyLower(`${i.pull.owner}/${i.pull.repo}`))
|
|
14290
|
+
);
|
|
14291
|
+
const [only2] = found;
|
|
14292
|
+
if (only2 === void 0) lines.push(said2.stopNone({ number }));
|
|
14293
|
+
else if (found.length > 1) lines.push(said2.stopMany({ number }));
|
|
14294
|
+
else lines.push(await this.stopReview(key, only2, zh));
|
|
13932
14295
|
}
|
|
13933
|
-
|
|
14296
|
+
await chat.say(lines.join("\n"));
|
|
13934
14297
|
}
|
|
13935
|
-
async
|
|
13936
|
-
const {
|
|
13937
|
-
const
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
14298
|
+
async stopReview(key, item, zh) {
|
|
14299
|
+
const pull2 = item.pull ?? { owner: "", repo: "", number: 0 };
|
|
14300
|
+
const short = `${pull2.owner}/${pull2.repo}#${String(pull2.number)}`;
|
|
14301
|
+
let error;
|
|
14302
|
+
try {
|
|
14303
|
+
const decision = await this.ctx.deps.claw.workItems.stop(key, item.itemId);
|
|
14304
|
+
error = decision.ok ? "" : decision.error;
|
|
14305
|
+
if (decision.ok) this.ctx.itemStopped(item.itemId);
|
|
14306
|
+
} catch (failure) {
|
|
14307
|
+
if (failure instanceof ClawUnreachable)
|
|
14308
|
+
return zh ? `${short}\uFF1A\u6CA1\u8FDE\u4E0A PrimusClaw\uFF0C\u8BF7\u518D\u8BF4\u4E00\u6B21\u3002` : `${short}: PrimusClaw did not answer; please say it again.`;
|
|
14309
|
+
if (!(failure instanceof ClawError)) throw failure;
|
|
14310
|
+
error = failure.status === 409 && isErrorWord(failure.code) ? failure.code : `error ${String(failure.status)}`;
|
|
13944
14311
|
}
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
if (!this.ctx.deps.exchange.enabled || !usesExchange) return void 0;
|
|
13951
|
-
return () => this.ctx.credentials.fresh(request.chat.sender.id, request.credential);
|
|
14312
|
+
const said2 = (Object.hasOwn(STOP_SAID, error) ? STOP_SAID[error] : void 0) ?? [
|
|
14313
|
+
`\u6CA1\u80FD\u505C\u4E0B\uFF08${error}\uFF09\u3002`,
|
|
14314
|
+
`it could not be stopped (${error}).`
|
|
14315
|
+
];
|
|
14316
|
+
return zh ? `${short}\uFF1A${said2[0]}` : `${short}: ${said2[1]}`;
|
|
13952
14317
|
}
|
|
13953
|
-
|
|
13954
|
-
|
|
14318
|
+
};
|
|
14319
|
+
|
|
14320
|
+
// packages/core/src/routing/link-commands.ts
|
|
14321
|
+
var LinkCommands = class {
|
|
14322
|
+
constructor(ctx, resume2) {
|
|
14323
|
+
this.ctx = ctx;
|
|
14324
|
+
this.resume = resume2;
|
|
13955
14325
|
}
|
|
13956
|
-
|
|
13957
|
-
|
|
13958
|
-
|
|
14326
|
+
ctx;
|
|
14327
|
+
resume;
|
|
14328
|
+
async link(chat, code3, messageId) {
|
|
13959
14329
|
const { store } = this.ctx.deps;
|
|
13960
|
-
const
|
|
13961
|
-
const
|
|
13962
|
-
|
|
13963
|
-
|
|
13964
|
-
|
|
13965
|
-
|
|
13966
|
-
};
|
|
13967
|
-
|
|
13968
|
-
|
|
13969
|
-
|
|
13970
|
-
const { store,
|
|
13971
|
-
const
|
|
13972
|
-
const
|
|
13973
|
-
|
|
13974
|
-
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
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);
|
|
14330
|
+
const outcome = store.links.redeemPending(chat.sender.id, chat.sender.name, code3 ?? "", messageId || void 0);
|
|
14331
|
+
const said2 = linkText.in("en");
|
|
14332
|
+
if (outcome === "ok") await this.linked(chat);
|
|
14333
|
+
else if (outcome === "limited") {
|
|
14334
|
+
const minutes = Math.max(1, Math.ceil(store.links.linkWindowSeconds / 60));
|
|
14335
|
+
await chat.say(said2.tooManyWrong({ minutes }));
|
|
14336
|
+
} else if (outcome === "global_limit") await chat.say(said2.linkingPaused);
|
|
14337
|
+
else await chat.say(said2.badCode);
|
|
14338
|
+
}
|
|
14339
|
+
async linked(chat) {
|
|
14340
|
+
const { store, subscriptions } = this.ctx.deps;
|
|
14341
|
+
const userId = chat.sender.id;
|
|
14342
|
+
const key = store.links.getKey(userId) ?? "";
|
|
14343
|
+
const clawUserId = key ? await this.ctx.owners.userIdOf(key) : void 0;
|
|
14344
|
+
if (clawUserId !== void 0) store.links.setClawUserId(userId, clawUserId);
|
|
14345
|
+
if (subscriptions !== void 0) await subscriptions.ensure(userId);
|
|
14346
|
+
const pending = store.onboarding.takePendingRequest(userId);
|
|
14347
|
+
const zh = chat.zh();
|
|
14348
|
+
const said2 = linkText.in(zh ? "zh" : "en");
|
|
14349
|
+
let who;
|
|
14350
|
+
if (zh) {
|
|
14351
|
+
const owner = key ? await this.ctx.owners.whoami(key) : void 0;
|
|
14352
|
+
who = owner === void 0 ? "" : `\u5230 SaFE \u7528\u6237 ${owner[0]}` + (owner[1] ? `\uFF08${owner[1]}\uFF09` : "");
|
|
14353
|
+
} else {
|
|
14354
|
+
who = await this.ctx.owners.whoLine(key) ?? "";
|
|
14032
14355
|
}
|
|
14356
|
+
await chat.say(said2.linkedAs({ who }) + (pending === null ? said2.getStarted : said2.runningFromGroup));
|
|
14357
|
+
if (pending !== null) await this.resume(chat, pending, true);
|
|
14033
14358
|
}
|
|
14034
|
-
async
|
|
14035
|
-
|
|
14036
|
-
|
|
14037
|
-
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
14359
|
+
async unlink(chat) {
|
|
14360
|
+
const { store, remote, assistant, subscriptions, keyRevoker } = this.ctx.deps;
|
|
14361
|
+
const userId = chat.sender.id;
|
|
14362
|
+
remote.forgetPerson(userId);
|
|
14363
|
+
assistant.forget(userId);
|
|
14364
|
+
const stored2 = store.links.getKey(userId);
|
|
14365
|
+
if (stored2 === null) {
|
|
14366
|
+
store.sessions.clearSession(userId);
|
|
14367
|
+
await chat.say(unlinkText.in(chat.lang()).bindingOnly);
|
|
14368
|
+
return;
|
|
14041
14369
|
}
|
|
14370
|
+
if (subscriptions !== void 0) await subscriptions.remove(userId, stored2);
|
|
14371
|
+
const revoked = await keyRevoker.revoke(stored2);
|
|
14372
|
+
store.links.unlink(userId);
|
|
14373
|
+
const stillBound = await this.stillBound(userId);
|
|
14374
|
+
const said2 = unlinkText.in(chat.lang());
|
|
14375
|
+
const tail = (revoked ? said2.deleted : said2.notDeleted) + (stillBound ? said2.stillBound : "");
|
|
14376
|
+
await chat.say(said2.unlinked({ tail }));
|
|
14377
|
+
}
|
|
14378
|
+
/** A chat that runs on the exchange stays connected through the person's binding, which only they can remove. */
|
|
14379
|
+
async stillBound(userId) {
|
|
14380
|
+
if (!this.ctx.credentials.onExchange) return false;
|
|
14381
|
+
this.ctx.deps.exchange.invalidate(userId);
|
|
14382
|
+
return (await this.ctx.deps.exchange.tokenFor(userId)).ok;
|
|
14042
14383
|
}
|
|
14043
14384
|
/**
|
|
14044
|
-
* The
|
|
14045
|
-
*
|
|
14385
|
+
* The person pressed the button that says they confirmed: the binding is looked for now. The worker cannot make it,
|
|
14386
|
+
* only the person can; not there yet, the card comes back with the link that is still open (or a new one).
|
|
14046
14387
|
*/
|
|
14047
|
-
|
|
14048
|
-
const { store,
|
|
14049
|
-
const
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
|
|
14053
|
-
chat.
|
|
14054
|
-
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
|
|
14061
|
-
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14388
|
+
async confirmBinding(action, chat) {
|
|
14389
|
+
const { exchange, store, logger } = this.ctx.deps;
|
|
14390
|
+
const said2 = linkText.in("en");
|
|
14391
|
+
exchange.invalidate(chat.sender.id);
|
|
14392
|
+
const token = await exchange.tokenFor(chat.sender.id);
|
|
14393
|
+
if (!token.ok && token.error.kind === "not_bound") {
|
|
14394
|
+
const notYet = linkText.in(chat.lang());
|
|
14395
|
+
const card = await this.ctx.credentials.bindingCard(chat, notYet.notConfirmedYet);
|
|
14396
|
+
await this.ctx.settle(action, chat, card, { fallback: notYet.notConfirmedLine });
|
|
14397
|
+
return;
|
|
14398
|
+
}
|
|
14399
|
+
if (!token.ok) {
|
|
14400
|
+
logger.warn("token exchange failed", { kind: token.error.kind });
|
|
14401
|
+
const text8 = said2.platformUnreachable;
|
|
14402
|
+
const card = this.ctx.views.note(`${text8}${said2.pressConfirmAgain}`, {
|
|
14403
|
+
buttons: [CONFIRM_BUTTON],
|
|
14404
|
+
title: said2.connectTitle({ bot: this.ctx.deps.config.botName })
|
|
14405
|
+
});
|
|
14406
|
+
await this.ctx.settle(action, chat, card, { fallback: text8 });
|
|
14407
|
+
return;
|
|
14408
|
+
}
|
|
14409
|
+
const pending = store.onboarding.takePendingRequest(chat.sender.id);
|
|
14410
|
+
await this.ctx.settle(action, chat, said2.connected + (pending === null ? said2.getStarted : said2.runningFromGroup));
|
|
14411
|
+
if (pending !== null) await this.resume(chat, pending, true);
|
|
14066
14412
|
}
|
|
14067
14413
|
};
|
|
14068
14414
|
|
|
@@ -14228,22 +14574,6 @@ var Router = class _Router {
|
|
|
14228
14574
|
}
|
|
14229
14575
|
};
|
|
14230
14576
|
|
|
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
14577
|
// packages/core/src/approvals/sensitive.ts
|
|
14248
14578
|
var WORDS = /* @__PURE__ */ new Set([
|
|
14249
14579
|
"token",
|
|
@@ -14510,145 +14840,6 @@ function approvalView(request) {
|
|
|
14510
14840
|
};
|
|
14511
14841
|
}
|
|
14512
14842
|
|
|
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
14843
|
// packages/core/src/approvals/decide.ts
|
|
14653
14844
|
function str(value) {
|
|
14654
14845
|
return typeof value === "string" ? value : typeof value === "number" ? String(value) : "";
|
|
@@ -14951,16 +15142,18 @@ var ApprovalCards = class {
|
|
|
14951
15142
|
var TURN_DEADLINE_SECONDS2 = 30 * 60;
|
|
14952
15143
|
function turnApprovals(store) {
|
|
14953
15144
|
return {
|
|
14954
|
-
record: (userId, sessionId, toolUseId, tool2, expiresIn, envelope) => store.approvals.saveApproval({
|
|
15145
|
+
record: (userId, sessionId, toolUseId, tool2, expiresIn, envelope, questions) => store.approvals.saveApproval({
|
|
14955
15146
|
userId,
|
|
14956
15147
|
sessionId,
|
|
14957
15148
|
toolUseId,
|
|
14958
15149
|
tool: tool2,
|
|
14959
15150
|
expiresAt: store.now() + (expiresIn || TURN_DEADLINE_SECONDS2),
|
|
14960
|
-
envelope
|
|
15151
|
+
envelope,
|
|
15152
|
+
// (a question's, as its card shows them: what a typed answer is matched against)
|
|
15153
|
+
...questions === void 0 ? {} : { questions: cardQuestions(questions).map((q) => questionWire(q)) }
|
|
14961
15154
|
}),
|
|
14962
|
-
sent: (sessionId, toolUseId) => {
|
|
14963
|
-
store.approvals.approvalSent(sessionId, toolUseId);
|
|
15155
|
+
sent: (sessionId, toolUseId, cardId) => {
|
|
15156
|
+
store.approvals.approvalSent(sessionId, toolUseId, cardId);
|
|
14964
15157
|
},
|
|
14965
15158
|
unsent: (sessionId, toolUseId) => {
|
|
14966
15159
|
store.approvals.forgetApproval(sessionId, toolUseId);
|
|
@@ -14982,7 +15175,15 @@ var approvalTurnCards = {
|
|
|
14982
15175
|
return viewMessage2(
|
|
14983
15176
|
{
|
|
14984
15177
|
name: "question",
|
|
14985
|
-
model: {
|
|
15178
|
+
model: {
|
|
15179
|
+
questions: cardQuestions(ask3.questions),
|
|
15180
|
+
sessionId,
|
|
15181
|
+
toolUseId,
|
|
15182
|
+
group,
|
|
15183
|
+
ownerName,
|
|
15184
|
+
note: "",
|
|
15185
|
+
typedReply: true
|
|
15186
|
+
}
|
|
14986
15187
|
},
|
|
14987
15188
|
QUESTION_TOOL2
|
|
14988
15189
|
);
|
|
@@ -32844,6 +33045,7 @@ export {
|
|
|
32844
33045
|
casefold,
|
|
32845
33046
|
channelActions,
|
|
32846
33047
|
channelContext,
|
|
33048
|
+
chatCredentialOf,
|
|
32847
33049
|
chatTurnKey,
|
|
32848
33050
|
clawText,
|
|
32849
33051
|
cleanText,
|
|
@@ -32947,6 +33149,7 @@ export {
|
|
|
32947
33149
|
lastTurns,
|
|
32948
33150
|
learnLang,
|
|
32949
33151
|
limitsText,
|
|
33152
|
+
logTurnStarted,
|
|
32950
33153
|
looksInternal,
|
|
32951
33154
|
looksLikeFailure,
|
|
32952
33155
|
looksLikeRepo,
|
|
@@ -33147,6 +33350,7 @@ export {
|
|
|
33147
33350
|
turnIdOf,
|
|
33148
33351
|
two,
|
|
33149
33352
|
typedAnswerText,
|
|
33353
|
+
typedAnswers,
|
|
33150
33354
|
typedMode,
|
|
33151
33355
|
unique,
|
|
33152
33356
|
unreachable,
|