@mrciphersmith/keryx 0.3.110 → 0.3.111
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/README.md +15 -3
- package/dist/cli.js +581 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1060,13 +1060,25 @@ runs. At most 500 lines wait per topic, and turning it off tells the topic which
|
|
|
1060
1060
|
queued lines will not run. From the topic you can also run a fixed list of slash
|
|
1061
1061
|
commands (`/status`, `/model` and `/connect` as buttons, `/new`, `/clear`, `/resume`
|
|
1062
1062
|
in the same topic); `/mode trust`, `/plan off`, `/delegate` and `/external*` run only
|
|
1063
|
-
after a Yes press
|
|
1064
|
-
`/
|
|
1065
|
-
|
|
1063
|
+
after a Yes press (so do `/route on` and `/route off`; bare `/route` shows at once),
|
|
1064
|
+
and commands such as `/mcp`, `/guard`, `/provider`, `/schedule`, `/rewind`,
|
|
1065
|
+
`/conform` and `/ci` stay local. When main is busy, `/route`, `/external`,
|
|
1066
|
+
`/external-agents` and `/model` are queued with a notice in the topic and run when
|
|
1067
|
+
it goes idle; `/mode` takes effect at once. A topic is archived when the shell
|
|
1068
|
+
exits (a closing note, the topic and its history stay readable), but a resumed
|
|
1069
|
+
session gets a new one, so `/history [N]` (1 to 20, 10 by default) posts the session's last messages
|
|
1066
1070
|
back into the topic, yours and the agent's final answers, one message each; a resumed
|
|
1067
1071
|
session whose topic was just created gets the last 10 by itself, once (the first time remote control is turned on in that shell); a turn that looks like it holds a secret is replaced by a placeholder. Each message shows its state as a reaction and the topic shows typing
|
|
1068
1072
|
while a turn runs; see
|
|
1069
1073
|
[commands from the topic](docs/docs/guides/drive-keryx-remotely.md#commands-from-the-topic).
|
|
1074
|
+
Every approval, `ask_user` question and queue dialog the shell raises is posted to
|
|
1075
|
+
the topic with buttons, whoever started the turn; the first answer wins, from the
|
|
1076
|
+
terminal or the topic, and an unanswered one gets a reminder every 2 minutes (at most 3).
|
|
1077
|
+
While a turn runs, the topic gets at most one progress note a minute (at wave
|
|
1078
|
+
boundaries and after tool-heavy stretches: counts and tool names, never tool output),
|
|
1079
|
+
and a short status question such as "how is it going?" is answered from session state
|
|
1080
|
+
without waiting for the turn; see
|
|
1081
|
+
[What the topic hears](docs/docs/guides/drive-keryx-remotely.md#what-the-topic-hears).
|
|
1070
1082
|
Without a connection `serve` starts as before, with remote control off, and only
|
|
1071
1083
|
offers the local `/channels` routes. Replies are rendered in one of four modes,
|
|
1072
1084
|
`remote.rendering` = `auto` (the default), `rich`, `html` or `plain`, set from the
|
package/dist/cli.js
CHANGED
|
@@ -21147,7 +21147,7 @@ var package_default;
|
|
|
21147
21147
|
var init_package = __esm(() => {
|
|
21148
21148
|
package_default = {
|
|
21149
21149
|
name: "@mrciphersmith/keryx",
|
|
21150
|
-
version: "0.3.
|
|
21150
|
+
version: "0.3.111",
|
|
21151
21151
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
21152
21152
|
private: false,
|
|
21153
21153
|
publishConfig: {
|
|
@@ -123364,7 +123364,7 @@ class SseParser {
|
|
|
123364
123364
|
return frames;
|
|
123365
123365
|
}
|
|
123366
123366
|
}
|
|
123367
|
-
var REMOTE_API_PREFIX = "/v1/remote", REMOTE_SCHEMA_VERSION = "1.0.0", REMOTE_ROUTE_METHODS, CHANNELS_ROUTE_METHODS, MAX_REMOTE_BODY_BYTES, MAX_PROJECT_CHARS = 256, MAX_APPROVAL_PROMPT_CHARS = 3000, MAX_KEYBOARD_ROWS = 8, MAX_KEYBOARD_BUTTONS_PER_ROW = 8, MAX_BUTTON_TEXT_CHARS = 64, MAX_CALLBACK_DATA_BYTES = 64, DEFAULT_APPROVAL_TIMEOUT_MS, MAX_APPROVAL_TIMEOUT_MS, MIN_APPROVAL_TIMEOUT_MS = 100, DEFAULT_APPROVAL_ACK_MS = 5000, DEFAULT_KEEPALIVE_MS = 15000, DEFAULT_ACK_TIMEOUT_MS = 30000, SESSION_ID_PATTERN, APPROVAL_ID_PATTERN, APPROVAL_CALLBACK_PATTERN, RESERVED_CALLBACK_PREFIX = "ap:", PROMPT_ID_PATTERN, CHOICE_CALLBACK_PATTERN, OWN_ANSWER_BUTTON_LABEL = "\u270D \u0421\u0432\u043E\u0439 \u043E\u0442\u0432\u0435\u0442", OWN_ANSWER_WINDOW_MS, OWN_REPLY_PROMPT = "Reply to this message with your own answer. It is open for 5 minutes.", MAX_OWN_ANSWER_CHARS = 4096, OWN_CALLBACK_PATTERN, RESERVED_CHOICE_PREFIX = "pk:", MAX_PROMPT_BUTTONS = 24, INTAKE_SERVICE_TOPIC = "Intake", RESERVED_INTAKE_PREFIX = "in:", MAX_REMEMBER_PATTERN_CHARS = 300, APPROVAL_INPUT_PREVIEW_CHARS = 1500, MESSAGE_STATES, SSE_KEEPALIVE_FRAME = `: keepalive
|
|
123367
|
+
var REMOTE_API_PREFIX = "/v1/remote", REMOTE_SCHEMA_VERSION = "1.0.0", REMOTE_ROUTE_METHODS, CHANNELS_ROUTE_METHODS, MAX_REMOTE_BODY_BYTES, MAX_PROJECT_CHARS = 256, MAX_APPROVAL_PROMPT_CHARS = 3000, MAX_KEYBOARD_ROWS = 8, MAX_KEYBOARD_BUTTONS_PER_ROW = 8, MAX_BUTTON_TEXT_CHARS = 64, MAX_CALLBACK_DATA_BYTES = 64, DEFAULT_APPROVAL_TIMEOUT_MS, MAX_APPROVAL_TIMEOUT_MS, MIN_APPROVAL_TIMEOUT_MS = 100, DEFAULT_PROMPT_REMINDER_MS, MAX_PROMPT_REMINDERS = 3, DEFAULT_APPROVAL_ACK_MS = 5000, DEFAULT_KEEPALIVE_MS = 15000, DEFAULT_ACK_TIMEOUT_MS = 30000, SESSION_ID_PATTERN, APPROVAL_ID_PATTERN, APPROVAL_CALLBACK_PATTERN, RESERVED_CALLBACK_PREFIX = "ap:", PROMPT_ID_PATTERN, CHOICE_CALLBACK_PATTERN, OWN_ANSWER_BUTTON_LABEL = "\u270D \u0421\u0432\u043E\u0439 \u043E\u0442\u0432\u0435\u0442", OWN_ANSWER_WINDOW_MS, OWN_REPLY_PROMPT = "Reply to this message with your own answer. It is open for 5 minutes.", MAX_OWN_ANSWER_CHARS = 4096, OWN_CALLBACK_PATTERN, RESERVED_CHOICE_PREFIX = "pk:", MAX_PROMPT_BUTTONS = 24, INTAKE_SERVICE_TOPIC = "Intake", RESERVED_INTAKE_PREFIX = "in:", MAX_REMEMBER_PATTERN_CHARS = 300, APPROVAL_INPUT_PREVIEW_CHARS = 1500, MESSAGE_STATES, SSE_KEEPALIVE_FRAME = `: keepalive
|
|
123368
123368
|
|
|
123369
123369
|
`;
|
|
123370
123370
|
var init_protocol2 = __esm(() => {
|
|
@@ -123394,6 +123394,7 @@ var init_protocol2 = __esm(() => {
|
|
|
123394
123394
|
MAX_REMOTE_BODY_BYTES = 64 * 1024;
|
|
123395
123395
|
DEFAULT_APPROVAL_TIMEOUT_MS = 5 * 60000;
|
|
123396
123396
|
MAX_APPROVAL_TIMEOUT_MS = 60 * 60000;
|
|
123397
|
+
DEFAULT_PROMPT_REMINDER_MS = 2 * 60000;
|
|
123397
123398
|
SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
123398
123399
|
APPROVAL_ID_PATTERN = /^ap[0-9a-f]{12}$/;
|
|
123399
123400
|
APPROVAL_CALLBACK_PATTERN = /^ap:(ap[0-9a-f]{12}):(allow|deny|always)$/;
|
|
@@ -123707,6 +123708,9 @@ var init_shell_token = __esm(() => {
|
|
|
123707
123708
|
});
|
|
123708
123709
|
|
|
123709
123710
|
// src/remote/client.ts
|
|
123711
|
+
function isAborted(signal) {
|
|
123712
|
+
return signal?.aborted === true;
|
|
123713
|
+
}
|
|
123710
123714
|
function closeByFor(signal) {
|
|
123711
123715
|
return signal?.reason === "cancelled" ? "cancelled" : "shell";
|
|
123712
123716
|
}
|
|
@@ -123931,6 +123935,10 @@ class RemoteClient {
|
|
|
123931
123935
|
if (!this.connected) {
|
|
123932
123936
|
return { decision: "deny" };
|
|
123933
123937
|
}
|
|
123938
|
+
if (isAborted(options.signal)) {
|
|
123939
|
+
await this.closePrompt(approvalId, closeByFor(options.signal));
|
|
123940
|
+
return { decision: "deny", approvalId };
|
|
123941
|
+
}
|
|
123934
123942
|
return new Promise((resolve) => {
|
|
123935
123943
|
const signal = options.signal;
|
|
123936
123944
|
const onAbort = () => {
|
|
@@ -123939,6 +123947,7 @@ class RemoteClient {
|
|
|
123939
123947
|
clearTimeout(waiter.timer);
|
|
123940
123948
|
this.waiters.delete(approvalId);
|
|
123941
123949
|
resolve({ decision: "deny", approvalId });
|
|
123950
|
+
this.closePrompt(approvalId, closeByFor(signal));
|
|
123942
123951
|
};
|
|
123943
123952
|
const timer = setTimeout(() => {
|
|
123944
123953
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -124520,7 +124529,7 @@ function classifyRemoteCommand(line) {
|
|
|
124520
124529
|
const spec = BY_NAME.get(canonical) ?? BY_MENU.get(token);
|
|
124521
124530
|
if (canonical === "remote-control") {
|
|
124522
124531
|
const sub = args.split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
124523
|
-
return refuse3(canonical, sub === "off" ? "it would
|
|
124532
|
+
return refuse3(canonical, sub === "off" ? "it would archive this topic; turn it off in the shell" : refusedReason ?? "its panel is local", true);
|
|
124524
124533
|
}
|
|
124525
124534
|
if (spec === undefined) {
|
|
124526
124535
|
if (refusedReason !== undefined) {
|
|
@@ -124565,6 +124574,16 @@ function classifyRemoteCommand(line) {
|
|
|
124565
124574
|
};
|
|
124566
124575
|
}
|
|
124567
124576
|
return first === "" ? { kind: "run", command: name, line: "/external" } : refuse3(name, "use /external, /external on or /external off", true);
|
|
124577
|
+
case "route":
|
|
124578
|
+
if (first === "on" || first === "off") {
|
|
124579
|
+
return {
|
|
124580
|
+
kind: "confirm",
|
|
124581
|
+
command: name,
|
|
124582
|
+
line: `/route ${first}`,
|
|
124583
|
+
summary: `Switch model routing ${first}? This changes which model answers each turn.`
|
|
124584
|
+
};
|
|
124585
|
+
}
|
|
124586
|
+
return first === "" ? { kind: "run", command: name, line: "/route" } : refuse3(name, "use /route, /route on or /route off", true);
|
|
124568
124587
|
case "external-agents":
|
|
124569
124588
|
if (first === "on" || first === "off") {
|
|
124570
124589
|
return {
|
|
@@ -124619,7 +124638,10 @@ function remoteHelpText() {
|
|
|
124619
124638
|
function isBusyDeferred(name) {
|
|
124620
124639
|
return BUSY_DEFERRED_COMMANDS.includes(name);
|
|
124621
124640
|
}
|
|
124622
|
-
|
|
124641
|
+
function isQueuedWhenBusy(name) {
|
|
124642
|
+
return QUEUED_WHEN_BUSY_COMMANDS.includes(name);
|
|
124643
|
+
}
|
|
124644
|
+
var REMOTE_COMMANDS, REMOTE_REFUSED, BUSY_DEFERRED_COMMANDS, QUEUED_WHEN_BUSY_COMMANDS, BUSY_REASON = "main is busy: command deferred", BY_NAME, BY_MENU;
|
|
124623
124645
|
var init_command_gateway = __esm(() => {
|
|
124624
124646
|
REMOTE_COMMANDS = [
|
|
124625
124647
|
{ name: "help", kind: "builtin", description: "List the commands you can use from this topic" },
|
|
@@ -124650,7 +124672,8 @@ var init_command_gateway = __esm(() => {
|
|
|
124650
124672
|
{ name: "scenarios", kind: "text", description: "User scenarios the working diff likely changes" },
|
|
124651
124673
|
{ name: "delegate", kind: "confirm", description: "Hand a paid task to an external agent: /delegate <agent> <task>; asks first" },
|
|
124652
124674
|
{ name: "external", kind: "confirm", description: "Show or switch sending work to external providers; on and off ask first" },
|
|
124653
|
-
{ name: "external-agents", kind: "confirm", description: "Show or switch the external agent runtime; on and off ask first" }
|
|
124675
|
+
{ name: "external-agents", kind: "confirm", description: "Show or switch the external agent runtime; on and off ask first" },
|
|
124676
|
+
{ name: "route", kind: "confirm", description: "Show or switch model routing: /route [on|off]; on and off ask first" }
|
|
124654
124677
|
];
|
|
124655
124678
|
REMOTE_REFUSED = {
|
|
124656
124679
|
exit: "it would close the shell",
|
|
@@ -124659,14 +124682,13 @@ var init_command_gateway = __esm(() => {
|
|
|
124659
124682
|
provider: "it asks for provider keys and addresses, which are never typed into a chat",
|
|
124660
124683
|
"search-provider": "it asks for provider keys, which are never typed into a chat",
|
|
124661
124684
|
"search-connect": "it changes where searches are sent",
|
|
124662
|
-
"remote-control": "turning remote control off would
|
|
124685
|
+
"remote-control": "turning remote control off would archive this topic, and its panel is local",
|
|
124663
124686
|
integrate: "it edits other tools' configuration on this machine",
|
|
124664
124687
|
copy: "it uses this machine's clipboard",
|
|
124665
124688
|
game: "it needs the shell's screen",
|
|
124666
124689
|
setup: "it asks for keys and changes the configuration",
|
|
124667
124690
|
mcp: "it can trust tools and change server connections; use the shell",
|
|
124668
124691
|
guard: "it changes a safety guard; use the shell",
|
|
124669
|
-
route: "it changes where work is routed; use the shell",
|
|
124670
124692
|
editguard: "it changes a safety guard; use the shell",
|
|
124671
124693
|
settings: "it edits settings, some of them secret; use the shell",
|
|
124672
124694
|
bus: "it talks to other agents on this machine",
|
|
@@ -124695,7 +124717,8 @@ var init_command_gateway = __esm(() => {
|
|
|
124695
124717
|
ci: "it opens a panel in the shell",
|
|
124696
124718
|
jevprofile: "it opens a panel with toggles in the shell"
|
|
124697
124719
|
};
|
|
124698
|
-
BUSY_DEFERRED_COMMANDS = ["new", "clear", "resume", "sessions", "compact", "model", "connect"];
|
|
124720
|
+
BUSY_DEFERRED_COMMANDS = ["new", "clear", "resume", "sessions", "compact", "model", "connect", "route", "external", "external-agents"];
|
|
124721
|
+
QUEUED_WHEN_BUSY_COMMANDS = ["model", "route", "external", "external-agents"];
|
|
124699
124722
|
BY_NAME = new Map(REMOTE_COMMANDS.map((spec) => [spec.name, spec]));
|
|
124700
124723
|
BY_MENU = new Map(REMOTE_COMMANDS.map((spec) => [menuName(spec.name), spec]));
|
|
124701
124724
|
});
|
|
@@ -124873,10 +124896,42 @@ class RemoteCommandRouter {
|
|
|
124873
124896
|
this.ctx.reply(refusalText(decision.command, decision.reason)).catch(() => false);
|
|
124874
124897
|
}
|
|
124875
124898
|
enqueue(line, command, end) {
|
|
124899
|
+
if (isQueuedWhenBusy(command) && this.busyReason(line, command) !== undefined) {
|
|
124900
|
+
this.detach(async () => {
|
|
124901
|
+
if (await this.awaitIdle(line, command)) {
|
|
124902
|
+
this.enqueueNow(line, command, end);
|
|
124903
|
+
} else {
|
|
124904
|
+
ended(end, "failed");
|
|
124905
|
+
}
|
|
124906
|
+
});
|
|
124907
|
+
return;
|
|
124908
|
+
}
|
|
124909
|
+
this.enqueueNow(line, command, end);
|
|
124910
|
+
}
|
|
124911
|
+
enqueueNow(line, command, end) {
|
|
124876
124912
|
this.chain = this.chain.then(() => this.execute(command, line, async () => this.ctx.host.runCommand === undefined ? undefined : this.ctx.host.runCommand(line), end)).catch(() => {
|
|
124877
124913
|
return;
|
|
124878
124914
|
});
|
|
124879
124915
|
}
|
|
124916
|
+
async awaitIdle(line, command) {
|
|
124917
|
+
this.ctx.record(`/${command} queued: main is busy`);
|
|
124918
|
+
await this.ctx.reply(this.ctx.compose(`${line} is queued: main is busy. It runs when main is idle.`)).catch(() => false);
|
|
124919
|
+
const { idlePollMs = 250, queuedCommandLimitMs = 30 * 60000 } = this.ctx.limits;
|
|
124920
|
+
const until = Date.now() + queuedCommandLimitMs;
|
|
124921
|
+
while (this.ctx.host.isBusy()) {
|
|
124922
|
+
if (this.ctx.live?.() === false) {
|
|
124923
|
+
this.ctx.record(`${line} dropped: remote control went off while it waited`);
|
|
124924
|
+
return false;
|
|
124925
|
+
}
|
|
124926
|
+
if (Date.now() >= until) {
|
|
124927
|
+
this.ctx.record(`${line} dropped: main stayed busy`);
|
|
124928
|
+
await this.ctx.reply(`${line} was not run: main stayed busy for ${Math.max(1, Math.round(queuedCommandLimitMs / 60000))} minutes. Send it again when the run ends.`).catch(() => false);
|
|
124929
|
+
return false;
|
|
124930
|
+
}
|
|
124931
|
+
await new Promise((resolve) => setTimeout(resolve, idlePollMs));
|
|
124932
|
+
}
|
|
124933
|
+
return true;
|
|
124934
|
+
}
|
|
124880
124935
|
serial(work) {
|
|
124881
124936
|
const run = this.chain.then(work);
|
|
124882
124937
|
this.chain = run.then(() => {
|
|
@@ -125037,7 +125092,7 @@ Page ${page + 1} of ${pages}` : title;
|
|
|
125037
125092
|
async picker(command, fromId, end) {
|
|
125038
125093
|
const line = `/${command}`;
|
|
125039
125094
|
const busy = this.busyReason(line, command);
|
|
125040
|
-
if (busy !== undefined) {
|
|
125095
|
+
if (busy !== undefined && !isQueuedWhenBusy(command)) {
|
|
125041
125096
|
this.ctx.record(`${line} not run: ${busy}`);
|
|
125042
125097
|
await this.ctx.reply(`${line} was not run: ${busy}.`).catch(() => false);
|
|
125043
125098
|
return "failed";
|
|
@@ -125079,6 +125134,9 @@ Now: ${current.label}` : ""}`;
|
|
|
125079
125134
|
return "failed";
|
|
125080
125135
|
}
|
|
125081
125136
|
const switchModel = host.switchModel.bind(host);
|
|
125137
|
+
if (this.busyReason("/model", "model") !== undefined && !await this.awaitIdle(`/model ${chosen.label.replace(/^\* /, "")}`, "model")) {
|
|
125138
|
+
return "failed";
|
|
125139
|
+
}
|
|
125082
125140
|
const switched = await this.serial(async () => {
|
|
125083
125141
|
const busy = this.busyReason("/model", "model");
|
|
125084
125142
|
return busy !== undefined ? { busy } : await switchModel(chosen.id, providerId);
|
|
@@ -125167,10 +125225,240 @@ var DEFAULT_ROUTER_LIMITS, PICKER_PAGE_SIZE = 7, LABEL_CHARS = 56, SESSION_COMMA
|
|
|
125167
125225
|
var init_command_router = __esm(() => {
|
|
125168
125226
|
init_command_gateway();
|
|
125169
125227
|
init_history2();
|
|
125170
|
-
DEFAULT_ROUTER_LIMITS = {
|
|
125228
|
+
DEFAULT_ROUTER_LIMITS = {
|
|
125229
|
+
runningNoticeMs: 4000,
|
|
125230
|
+
commandLimitMs: 120000,
|
|
125231
|
+
choiceTimeoutMs: 120000,
|
|
125232
|
+
idlePollMs: 250,
|
|
125233
|
+
queuedCommandLimitMs: 30 * 60000
|
|
125234
|
+
};
|
|
125171
125235
|
SESSION_COMMANDS = ["new", "clear", "resume"];
|
|
125172
125236
|
});
|
|
125173
125237
|
|
|
125238
|
+
// src/remote/progress-notes.ts
|
|
125239
|
+
function formatElapsed(ms) {
|
|
125240
|
+
const total = Math.max(0, Math.round(ms / 1000));
|
|
125241
|
+
if (total < 60)
|
|
125242
|
+
return `${total}s`;
|
|
125243
|
+
const minutes = Math.floor(total / 60);
|
|
125244
|
+
const seconds = total % 60;
|
|
125245
|
+
if (minutes < 60)
|
|
125246
|
+
return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
|
125247
|
+
const hours = Math.floor(minutes / 60);
|
|
125248
|
+
return `${hours}h ${minutes % 60}m`;
|
|
125249
|
+
}
|
|
125250
|
+
|
|
125251
|
+
class ProgressNotes {
|
|
125252
|
+
intervalMs;
|
|
125253
|
+
threshold;
|
|
125254
|
+
now;
|
|
125255
|
+
send;
|
|
125256
|
+
setTimer;
|
|
125257
|
+
clearTimer;
|
|
125258
|
+
startedAt;
|
|
125259
|
+
toolCalls = 0;
|
|
125260
|
+
lastTool;
|
|
125261
|
+
sinceNote = 0;
|
|
125262
|
+
namesSinceNote = new Set;
|
|
125263
|
+
lastNoteAt;
|
|
125264
|
+
lastNoteWindowStart;
|
|
125265
|
+
pending;
|
|
125266
|
+
timer;
|
|
125267
|
+
wave = new Map;
|
|
125268
|
+
waveOpen = false;
|
|
125269
|
+
constructor(options) {
|
|
125270
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_PROGRESS_INTERVAL_MS;
|
|
125271
|
+
this.threshold = Math.max(1, options.toolThreshold ?? DEFAULT_PROGRESS_TOOL_THRESHOLD);
|
|
125272
|
+
this.now = options.now ?? Date.now;
|
|
125273
|
+
this.send = options.send;
|
|
125274
|
+
this.setTimer = options.setTimer ?? ((run, ms) => setTimeout(run, ms));
|
|
125275
|
+
this.clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
125276
|
+
}
|
|
125277
|
+
get enabled() {
|
|
125278
|
+
return this.intervalMs > 0;
|
|
125279
|
+
}
|
|
125280
|
+
begin() {
|
|
125281
|
+
this.dropPending();
|
|
125282
|
+
this.startedAt = this.now();
|
|
125283
|
+
this.toolCalls = 0;
|
|
125284
|
+
this.lastTool = undefined;
|
|
125285
|
+
this.sinceNote = 0;
|
|
125286
|
+
this.namesSinceNote.clear();
|
|
125287
|
+
this.wave.clear();
|
|
125288
|
+
this.waveOpen = false;
|
|
125289
|
+
this.lastNoteWindowStart = this.startedAt;
|
|
125290
|
+
}
|
|
125291
|
+
end() {
|
|
125292
|
+
this.dropPending();
|
|
125293
|
+
this.startedAt = undefined;
|
|
125294
|
+
this.wave.clear();
|
|
125295
|
+
this.waveOpen = false;
|
|
125296
|
+
}
|
|
125297
|
+
reset() {
|
|
125298
|
+
this.end();
|
|
125299
|
+
this.lastNoteAt = undefined;
|
|
125300
|
+
}
|
|
125301
|
+
tool(name) {
|
|
125302
|
+
if (this.startedAt === undefined)
|
|
125303
|
+
return;
|
|
125304
|
+
this.toolCalls += 1;
|
|
125305
|
+
this.sinceNote += 1;
|
|
125306
|
+
if (name !== undefined && name.length > 0) {
|
|
125307
|
+
this.lastTool = name;
|
|
125308
|
+
this.namesSinceNote.add(name);
|
|
125309
|
+
}
|
|
125310
|
+
if (this.sinceNote >= this.threshold)
|
|
125311
|
+
this.post({ priority: 1, build: () => this.toolNote() });
|
|
125312
|
+
}
|
|
125313
|
+
subagent(event) {
|
|
125314
|
+
if (this.startedAt === undefined)
|
|
125315
|
+
return;
|
|
125316
|
+
if (event.kind === "upsert") {
|
|
125317
|
+
this.wave.set(event.id, { status: event.status, label: event.label ?? event.id });
|
|
125318
|
+
if (!this.waveOpen && ACTIVE.has(event.status)) {
|
|
125319
|
+
this.waveOpen = true;
|
|
125320
|
+
this.post({ priority: 2, build: () => this.waveStartNote() });
|
|
125321
|
+
return;
|
|
125322
|
+
}
|
|
125323
|
+
} else if (event.kind === "remove") {
|
|
125324
|
+
this.wave.delete(event.id);
|
|
125325
|
+
} else {
|
|
125326
|
+
return;
|
|
125327
|
+
}
|
|
125328
|
+
if (this.waveOpen && this.activeCount() === 0) {
|
|
125329
|
+
const done = [...this.wave.values()].filter((one) => one.status === "done").length;
|
|
125330
|
+
const failed = [...this.wave.values()].filter((one) => one.status === "failed").length;
|
|
125331
|
+
this.waveOpen = false;
|
|
125332
|
+
this.wave.clear();
|
|
125333
|
+
this.post({ priority: 3, build: () => this.waveEndNote(done, failed) });
|
|
125334
|
+
}
|
|
125335
|
+
}
|
|
125336
|
+
snapshot() {
|
|
125337
|
+
const running = this.startedAt !== undefined;
|
|
125338
|
+
return {
|
|
125339
|
+
running,
|
|
125340
|
+
elapsedMs: running ? this.now() - this.startedAt : 0,
|
|
125341
|
+
toolCalls: this.toolCalls,
|
|
125342
|
+
...this.lastTool === undefined ? {} : { lastTool: this.lastTool },
|
|
125343
|
+
waveRunning: this.activeCount()
|
|
125344
|
+
};
|
|
125345
|
+
}
|
|
125346
|
+
activeCount() {
|
|
125347
|
+
let count = 0;
|
|
125348
|
+
for (const one of this.wave.values())
|
|
125349
|
+
if (ACTIVE.has(one.status))
|
|
125350
|
+
count += 1;
|
|
125351
|
+
return count;
|
|
125352
|
+
}
|
|
125353
|
+
elapsed() {
|
|
125354
|
+
return formatElapsed(this.startedAt === undefined ? 0 : this.now() - this.startedAt);
|
|
125355
|
+
}
|
|
125356
|
+
toolNote() {
|
|
125357
|
+
const names = [...this.namesSinceNote];
|
|
125358
|
+
const shown = names.slice(0, TOOL_NAMES_SHOWN).join(", ");
|
|
125359
|
+
const more = names.length > TOOL_NAMES_SHOWN ? ` and ${names.length - TOOL_NAMES_SHOWN} more` : "";
|
|
125360
|
+
const window = this.lastNoteWindowStart === undefined ? "" : ` in the last ${formatElapsed(this.now() - this.lastNoteWindowStart)}`;
|
|
125361
|
+
const used = shown.length > 0 ? ` (${shown}${more})` : "";
|
|
125362
|
+
return `Still working: ${this.sinceNote} tool calls${window}${used}. ${this.elapsed()} into this turn.`;
|
|
125363
|
+
}
|
|
125364
|
+
waveStartNote() {
|
|
125365
|
+
const labels = [...this.wave.values()].map((one) => one.label).slice(0, WAVE_LABELS_SHOWN);
|
|
125366
|
+
const total = this.activeCount();
|
|
125367
|
+
const more = total > labels.length ? ` and ${total - labels.length} more` : "";
|
|
125368
|
+
return `A subagent wave started: ${total} running${labels.length > 0 ? ` (${labels.join(", ")}${more})` : ""}. ${this.elapsed()} into this turn.`;
|
|
125369
|
+
}
|
|
125370
|
+
waveEndNote(done, failed) {
|
|
125371
|
+
const failedPart = failed > 0 ? `, ${failed} failed` : "";
|
|
125372
|
+
return `The wave finished: ${done} done${failedPart}. ${this.elapsed()} into this turn.`;
|
|
125373
|
+
}
|
|
125374
|
+
post(next) {
|
|
125375
|
+
if (!this.enabled)
|
|
125376
|
+
return;
|
|
125377
|
+
const at = this.now();
|
|
125378
|
+
if (this.lastNoteAt === undefined || at - this.lastNoteAt >= this.intervalMs) {
|
|
125379
|
+
this.emit(next);
|
|
125380
|
+
return;
|
|
125381
|
+
}
|
|
125382
|
+
if (this.pending === undefined || next.priority >= this.pending.priority)
|
|
125383
|
+
this.pending = next;
|
|
125384
|
+
if (this.timer === undefined) {
|
|
125385
|
+
this.timer = this.setTimer(() => {
|
|
125386
|
+
this.timer = undefined;
|
|
125387
|
+
const held = this.pending;
|
|
125388
|
+
this.pending = undefined;
|
|
125389
|
+
if (held !== undefined && this.startedAt !== undefined)
|
|
125390
|
+
this.emit(held);
|
|
125391
|
+
}, this.lastNoteAt + this.intervalMs - at);
|
|
125392
|
+
}
|
|
125393
|
+
}
|
|
125394
|
+
emit(note) {
|
|
125395
|
+
const text = note.build();
|
|
125396
|
+
this.lastNoteAt = this.now();
|
|
125397
|
+
this.lastNoteWindowStart = this.lastNoteAt;
|
|
125398
|
+
this.sinceNote = 0;
|
|
125399
|
+
this.namesSinceNote.clear();
|
|
125400
|
+
this.pending = undefined;
|
|
125401
|
+
try {
|
|
125402
|
+
this.send(text);
|
|
125403
|
+
} catch {}
|
|
125404
|
+
}
|
|
125405
|
+
dropPending() {
|
|
125406
|
+
this.pending = undefined;
|
|
125407
|
+
if (this.timer !== undefined) {
|
|
125408
|
+
this.clearTimer(this.timer);
|
|
125409
|
+
this.timer = undefined;
|
|
125410
|
+
}
|
|
125411
|
+
}
|
|
125412
|
+
}
|
|
125413
|
+
function isStatusQuestion(line) {
|
|
125414
|
+
const text = line.trim().toLowerCase().replace(/[?!.\u2026]+$/u, "").replace(/\s+/g, " ").trim();
|
|
125415
|
+
if (text.length === 0 || text.length > 60)
|
|
125416
|
+
return false;
|
|
125417
|
+
return STATUS_PATTERNS.some((pattern) => pattern.test(text));
|
|
125418
|
+
}
|
|
125419
|
+
function describeStatus(facts) {
|
|
125420
|
+
const lines = [];
|
|
125421
|
+
const { snapshot } = facts;
|
|
125422
|
+
if (facts.turnRunning && snapshot.running) {
|
|
125423
|
+
const tools = snapshot.toolCalls === 0 ? "no tool calls yet" : `${snapshot.toolCalls} tool call${snapshot.toolCalls === 1 ? "" : "s"}${snapshot.lastTool === undefined ? "" : `, last: ${snapshot.lastTool}`}`;
|
|
125424
|
+
lines.push(`Working: this turn has run for ${formatElapsed(snapshot.elapsedMs)}, ${tools}.`);
|
|
125425
|
+
} else if (facts.turnRunning) {
|
|
125426
|
+
lines.push("Working: a turn is running.");
|
|
125427
|
+
} else {
|
|
125428
|
+
lines.push("Busy: the shell is occupied with a command or its queue; no model turn is writing right now.");
|
|
125429
|
+
}
|
|
125430
|
+
if (snapshot.waveRunning > 0)
|
|
125431
|
+
lines.push(`Subagents: ${snapshot.waveRunning} still running.`);
|
|
125432
|
+
const queued = Math.max(facts.queued ?? 0, facts.topicWaiting);
|
|
125433
|
+
if (queued > 0) {
|
|
125434
|
+
lines.push(`Queue: ${queued} message${queued === 1 ? "" : "s"} waiting${facts.topicWaiting > 0 ? ` (${facts.topicWaiting} from this topic)` : ""}.`);
|
|
125435
|
+
}
|
|
125436
|
+
if (facts.openPrompts > 0) {
|
|
125437
|
+
lines.push(`Waiting for you: ${facts.openPrompts} approval or question open in this topic.`);
|
|
125438
|
+
}
|
|
125439
|
+
if (facts.mode !== undefined)
|
|
125440
|
+
lines.push(`Mode: ${facts.mode}.`);
|
|
125441
|
+
return lines.join(`
|
|
125442
|
+
`);
|
|
125443
|
+
}
|
|
125444
|
+
var DEFAULT_PROGRESS_INTERVAL_MS = 60000, DEFAULT_PROGRESS_TOOL_THRESHOLD = 8, TOOL_NAMES_SHOWN = 4, WAVE_LABELS_SHOWN = 3, ACTIVE, STATUS_PATTERNS;
|
|
125445
|
+
var init_progress_notes = __esm(() => {
|
|
125446
|
+
ACTIVE = new Set(["queued", "running", "blocked"]);
|
|
125447
|
+
STATUS_PATTERNS = [
|
|
125448
|
+
/^(status|progress|update)( (update|report|check))?( please| pls)?$/,
|
|
125449
|
+
/^(what'?s|what is|whats) (the )?(current )?(status|progress|going on|happening|up)( (now|right now|here))?$/,
|
|
125450
|
+
/^how('?s| is) (it|that|this|the (run|review|task|work|build)) (going|progressing|doing)( so far| now)?$/,
|
|
125451
|
+
/^(are you|r u) (still )?(working|running|busy|alive|there|done|finished|stuck)( yet| now)?$/,
|
|
125452
|
+
/^(is it|are we) (done|finished|ready|stuck)( yet)?$/,
|
|
125453
|
+
/^what are you (doing|working on)( now| right now)?$/,
|
|
125454
|
+
/^where are (you|we)( now| at)?$/,
|
|
125455
|
+
/^(any )?(news|progress|updates?)$/,
|
|
125456
|
+
/^still (working|running|there|alive)$/,
|
|
125457
|
+
/^(\u0441\u0442\u0430\u0442\u0443\u0441|\u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441|\u043A\u0430\u043A \u0434\u0435\u043B\u0430|\u043A\u0430\u043A \u0443\u0441\u043F\u0435\u0445\u0438|\u043A\u0430\u043A \u0438\u0434\u0451\u0442|\u043A\u0430\u043A \u0438\u0434\u0435\u0442|\u043A\u0430\u043A \u043F\u0440\u043E\u0434\u0432\u0438\u0433\u0430\u0435\u0442\u0441\u044F( \u0440\u0430\u0431\u043E\u0442\u0430)?|\u0447\u0442\u043E \u0442\u0430\u043C|\u0447\u0442\u043E \u043D\u043E\u0432\u043E\u0433\u043E|\u0447\u0442\u043E \u0434\u0435\u043B\u0430\u0435\u0448\u044C|\u0447\u0442\u043E \u043F\u0440\u043E\u0438\u0441\u0445\u043E\u0434\u0438\u0442|\u0447\u0442\u043E \u0441\u0435\u0439\u0447\u0430\u0441 \u043F\u0440\u043E\u0438\u0441\u0445\u043E\u0434\u0438\u0442)( (\u0441\u0435\u0439\u0447\u0430\u0441|\u0442\u0430\u043C|\u0443\u0436\u0435))*$/,
|
|
125458
|
+
/^(\u0442\u044B )?(\u0442\u0443\u0442|\u0436\u0438\u0432|\u0435\u0449\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C|\u0435\u0449\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C|\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C|\u0437\u0430\u0432\u0438\u0441|\u0437\u0430\u0432\u0438\u0441(\u043B\u0430)?|\u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B|\u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0430|\u0433\u043E\u0442\u043E\u0432|\u0433\u043E\u0442\u043E\u0432\u043E)( (\u0442\u0430\u043C|\u0443\u0436\u0435|\u0435\u0449\u0451|\u0435\u0449\u0435|\u0441\u0435\u0439\u0447\u0430\u0441))*$/
|
|
125459
|
+
];
|
|
125460
|
+
});
|
|
125461
|
+
|
|
125174
125462
|
// src/remote/shell-bridge.ts
|
|
125175
125463
|
function preview2(text) {
|
|
125176
125464
|
const one = redactSensitiveText(text).replace(/\s+/g, " ").trim();
|
|
@@ -125273,11 +125561,25 @@ class RemoteBridge {
|
|
|
125273
125561
|
lastHistory;
|
|
125274
125562
|
postingClient;
|
|
125275
125563
|
enabledOnce = false;
|
|
125564
|
+
progress;
|
|
125565
|
+
openPrompts = 0;
|
|
125276
125566
|
constructor(options) {
|
|
125277
125567
|
this.options = options;
|
|
125278
125568
|
this.host = options.host;
|
|
125279
125569
|
this.now = options.now ?? Date.now;
|
|
125280
125570
|
this.approvalOverrideMs = options.approvalTimeoutMs;
|
|
125571
|
+
this.progress = new ProgressNotes({
|
|
125572
|
+
...options.progressIntervalMs === undefined ? {} : { intervalMs: options.progressIntervalMs },
|
|
125573
|
+
...options.progressToolThreshold === undefined ? {} : { toolThreshold: options.progressToolThreshold },
|
|
125574
|
+
now: this.now,
|
|
125575
|
+
send: (text) => {
|
|
125576
|
+
const client = this.client;
|
|
125577
|
+
if (client === undefined || !client.connected)
|
|
125578
|
+
return;
|
|
125579
|
+
this.push("reply", "progress note");
|
|
125580
|
+
client.reply(composeReply(text)).catch(() => false);
|
|
125581
|
+
}
|
|
125582
|
+
});
|
|
125281
125583
|
this.router = new RemoteCommandRouter({
|
|
125282
125584
|
host: {
|
|
125283
125585
|
isBusy: () => this.host.isBusy(),
|
|
@@ -125292,6 +125594,7 @@ class RemoteBridge {
|
|
|
125292
125594
|
...this.host.resumeSession !== undefined ? { resumeSession: (id) => this.host.resumeSession(id) } : {}
|
|
125293
125595
|
},
|
|
125294
125596
|
reply: async (text) => this.client !== undefined ? await this.client.reply(text) : false,
|
|
125597
|
+
live: () => this.client !== undefined,
|
|
125295
125598
|
history: async (args) => await this.historyForCommand(args),
|
|
125296
125599
|
record: (text) => this.push("command", text),
|
|
125297
125600
|
compose: composeReply,
|
|
@@ -125326,7 +125629,9 @@ class RemoteBridge {
|
|
|
125326
125629
|
limits: {
|
|
125327
125630
|
runningNoticeMs: options.commandRunningNoticeMs ?? DEFAULT_ROUTER_LIMITS.runningNoticeMs,
|
|
125328
125631
|
commandLimitMs: options.commandLimitMs ?? DEFAULT_ROUTER_LIMITS.commandLimitMs,
|
|
125329
|
-
choiceTimeoutMs: options.choiceTimeoutMs ?? DEFAULT_ROUTER_LIMITS.choiceTimeoutMs
|
|
125632
|
+
choiceTimeoutMs: options.choiceTimeoutMs ?? DEFAULT_ROUTER_LIMITS.choiceTimeoutMs,
|
|
125633
|
+
...options.idlePollMs !== undefined ? { idlePollMs: options.idlePollMs } : {},
|
|
125634
|
+
...options.queuedCommandLimitMs !== undefined ? { queuedCommandLimitMs: options.queuedCommandLimitMs } : {}
|
|
125330
125635
|
}
|
|
125331
125636
|
});
|
|
125332
125637
|
}
|
|
@@ -125441,6 +125746,7 @@ class RemoteBridge {
|
|
|
125441
125746
|
this.clientConnected = false;
|
|
125442
125747
|
this.lastHistory = undefined;
|
|
125443
125748
|
this.endTurnState();
|
|
125749
|
+
this.progress.reset();
|
|
125444
125750
|
this.waiting.length = 0;
|
|
125445
125751
|
this.host.recordOff();
|
|
125446
125752
|
const dropped = this.host.dropQueuedTelegramLines?.() ?? [];
|
|
@@ -125491,6 +125797,10 @@ class RemoteBridge {
|
|
|
125491
125797
|
return;
|
|
125492
125798
|
}
|
|
125493
125799
|
this.push("line", `Telegram: ${preview2(line)}`);
|
|
125800
|
+
if (this.host.isBusy() && isStatusQuestion(line)) {
|
|
125801
|
+
this.answerStatus(meta.updateId);
|
|
125802
|
+
return;
|
|
125803
|
+
}
|
|
125494
125804
|
this.waiting.push({ updateId: meta.updateId, line, ...meta.fromId === undefined ? {} : { fromId: meta.fromId } });
|
|
125495
125805
|
if (this.host.isBusy()) {
|
|
125496
125806
|
this.host.enqueue(line);
|
|
@@ -125499,6 +125809,22 @@ class RemoteBridge {
|
|
|
125499
125809
|
this.host.runLine(line);
|
|
125500
125810
|
}
|
|
125501
125811
|
}
|
|
125812
|
+
answerStatus(updateId) {
|
|
125813
|
+
const client = this.client;
|
|
125814
|
+
if (client === undefined)
|
|
125815
|
+
return;
|
|
125816
|
+
this.push("command", "status question answered from session state");
|
|
125817
|
+
const state = this.host.sessionState?.();
|
|
125818
|
+
const text = describeStatus({
|
|
125819
|
+
turnRunning: this.host.turnRunning !== undefined ? this.host.turnRunning() : true,
|
|
125820
|
+
snapshot: this.progress.snapshot(),
|
|
125821
|
+
topicWaiting: this.waiting.length,
|
|
125822
|
+
...state === undefined ? {} : { queued: state.queued },
|
|
125823
|
+
openPrompts: this.openPrompts,
|
|
125824
|
+
...state?.mode === undefined ? {} : { mode: state.mode }
|
|
125825
|
+
});
|
|
125826
|
+
client.reply(composeReply(text)).then((sent) => this.report(updateId, sent ? "done" : "failed")).catch(() => this.report(updateId, "failed"));
|
|
125827
|
+
}
|
|
125502
125828
|
report(updateId, state) {
|
|
125503
125829
|
const client = this.client;
|
|
125504
125830
|
if (client?.reportState === undefined)
|
|
@@ -125608,6 +125934,8 @@ class RemoteBridge {
|
|
|
125608
125934
|
}
|
|
125609
125935
|
turnStarted(source) {
|
|
125610
125936
|
this.endTurnState();
|
|
125937
|
+
if (this.client !== undefined)
|
|
125938
|
+
this.progress.begin();
|
|
125611
125939
|
if (source !== TG_SOURCE || this.client === undefined)
|
|
125612
125940
|
return;
|
|
125613
125941
|
const next = this.waiting.shift();
|
|
@@ -125625,7 +125953,7 @@ class RemoteBridge {
|
|
|
125625
125953
|
this.runTimer = setTimeout(() => {
|
|
125626
125954
|
this.timedOut = true;
|
|
125627
125955
|
this.push("error", "run time limit reached; stopping the turn");
|
|
125628
|
-
this.turnAbort?.abort();
|
|
125956
|
+
this.turnAbort?.abort("cancelled");
|
|
125629
125957
|
this.host.cancelTurn();
|
|
125630
125958
|
}, limit);
|
|
125631
125959
|
}
|
|
@@ -125639,7 +125967,7 @@ class RemoteBridge {
|
|
|
125639
125967
|
return "stopped";
|
|
125640
125968
|
this.stoppedByUser = true;
|
|
125641
125969
|
this.push("command", "/stop: stopping the turn");
|
|
125642
|
-
this.turnAbort?.abort();
|
|
125970
|
+
this.turnAbort?.abort("cancelled");
|
|
125643
125971
|
this.host.cancelTurn();
|
|
125644
125972
|
return "stopped";
|
|
125645
125973
|
}
|
|
@@ -125649,7 +125977,8 @@ class RemoteBridge {
|
|
|
125649
125977
|
if (text.trim().length > 0)
|
|
125650
125978
|
this.lastAssistantText = text;
|
|
125651
125979
|
}
|
|
125652
|
-
toolCall() {
|
|
125980
|
+
toolCall(name) {
|
|
125981
|
+
this.progress.tool(name);
|
|
125653
125982
|
if (!this.tgTurn)
|
|
125654
125983
|
return;
|
|
125655
125984
|
this.lastAssistantText = "";
|
|
@@ -125668,6 +125997,7 @@ class RemoteBridge {
|
|
|
125668
125997
|
client.reply(`Your message "${preview2(line)}" was removed from the queue in the shell, so it will not run.`).catch(() => false);
|
|
125669
125998
|
}
|
|
125670
125999
|
async turnSettled(outcome) {
|
|
126000
|
+
this.progress.end();
|
|
125671
126001
|
if (!this.tgTurn)
|
|
125672
126002
|
return;
|
|
125673
126003
|
const client = this.client;
|
|
@@ -125695,14 +126025,20 @@ class RemoteBridge {
|
|
|
125695
126025
|
const sent = await client.reply(body).catch(() => false);
|
|
125696
126026
|
this.push(sent ? "reply" : "error", sent ? `reply sent (${body.length} chars)` : "the reply could not be sent");
|
|
125697
126027
|
}
|
|
126028
|
+
subagentEvent(event) {
|
|
126029
|
+
if (this.client === undefined)
|
|
126030
|
+
return;
|
|
126031
|
+
this.progress.subagent(event);
|
|
126032
|
+
}
|
|
125698
126033
|
endTurnState() {
|
|
126034
|
+
this.progress.end();
|
|
125699
126035
|
this.tgTurn = false;
|
|
125700
126036
|
this.currentUpdate = undefined;
|
|
125701
126037
|
this.currentFromId = undefined;
|
|
125702
126038
|
this.lastAssistantText = "";
|
|
125703
126039
|
this.timedOut = false;
|
|
125704
126040
|
this.stoppedByUser = false;
|
|
125705
|
-
this.turnAbort?.abort();
|
|
126041
|
+
this.turnAbort?.abort("cancelled");
|
|
125706
126042
|
this.turnAbort = undefined;
|
|
125707
126043
|
if (this.runTimer !== undefined) {
|
|
125708
126044
|
clearTimeout(this.runTimer);
|
|
@@ -125720,11 +126056,13 @@ class RemoteBridge {
|
|
|
125720
126056
|
}
|
|
125721
126057
|
this.push("approval", "asked in the topic");
|
|
125722
126058
|
let answer;
|
|
126059
|
+
this.openPrompts += 1;
|
|
125723
126060
|
try {
|
|
125724
126061
|
const text = composeApprovalPrompt(prompt);
|
|
125725
|
-
const timeoutMs = this.approvalTimeoutMs;
|
|
126062
|
+
const timeoutMs = options.timeoutMs ?? this.approvalTimeoutMs;
|
|
125726
126063
|
if (client.askApproval !== undefined) {
|
|
125727
|
-
const
|
|
126064
|
+
const signals = [this.turnAbort?.signal, options.signal].filter((one) => one !== undefined);
|
|
126065
|
+
const signal = signals.length === 0 ? undefined : signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
125728
126066
|
const got = await client.askApproval(text, timeoutMs, {
|
|
125729
126067
|
...options.remember === undefined ? {} : { remember: options.remember },
|
|
125730
126068
|
...signal === undefined ? {} : { signal }
|
|
@@ -125736,6 +126074,8 @@ class RemoteBridge {
|
|
|
125736
126074
|
}
|
|
125737
126075
|
} catch {
|
|
125738
126076
|
answer = { decision: "deny", always: false };
|
|
126077
|
+
} finally {
|
|
126078
|
+
this.openPrompts -= 1;
|
|
125739
126079
|
}
|
|
125740
126080
|
const by = answer.fromId === undefined ? "" : ` by user ${answer.fromId}`;
|
|
125741
126081
|
this.push("approval", answer.decision === "allow" ? `${answer.always ? "allowed (always)" : "allowed"} in the topic${by}` : `denied${by === "" ? " (or no answer)" : by}`);
|
|
@@ -125756,6 +126096,7 @@ class RemoteBridge {
|
|
|
125756
126096
|
const timeoutMs = this.options.choiceTimeoutMs ?? DEFAULT_ROUTER_LIMITS.choiceTimeoutMs;
|
|
125757
126097
|
const forUserId = this.telegramTurnUserId;
|
|
125758
126098
|
this.push("command", "ask_user question asked in the topic");
|
|
126099
|
+
this.openPrompts += 1;
|
|
125759
126100
|
try {
|
|
125760
126101
|
if (client.askChoice === undefined) {
|
|
125761
126102
|
const index = await client.requestChoice(text, rows, timeoutMs, forUserId);
|
|
@@ -125773,6 +126114,8 @@ class RemoteBridge {
|
|
|
125773
126114
|
return;
|
|
125774
126115
|
} catch {
|
|
125775
126116
|
return;
|
|
126117
|
+
} finally {
|
|
126118
|
+
this.openPrompts -= 1;
|
|
125776
126119
|
}
|
|
125777
126120
|
}
|
|
125778
126121
|
async reportRemembered(approvalId, remembered) {
|
|
@@ -125788,9 +126131,49 @@ var init_shell_bridge = __esm(() => {
|
|
|
125788
126131
|
init_client3();
|
|
125789
126132
|
init_command_router();
|
|
125790
126133
|
init_protocol2();
|
|
126134
|
+
init_progress_notes();
|
|
125791
126135
|
init_history2();
|
|
125792
126136
|
});
|
|
125793
126137
|
|
|
126138
|
+
// src/tui/approval-mirror.ts
|
|
126139
|
+
function isTopicPress(answer) {
|
|
126140
|
+
return answer.decision === "allow" || answer.fromId !== undefined;
|
|
126141
|
+
}
|
|
126142
|
+
async function raceApproval(options) {
|
|
126143
|
+
const dockAbort = new AbortController;
|
|
126144
|
+
const topicAbort = new AbortController;
|
|
126145
|
+
let topicAnswer;
|
|
126146
|
+
let settled = false;
|
|
126147
|
+
let started = false;
|
|
126148
|
+
const race = {
|
|
126149
|
+
signal: AbortSignal.any([options.turnSignal, dockAbort.signal]),
|
|
126150
|
+
opened: () => {
|
|
126151
|
+
if (started || settled)
|
|
126152
|
+
return;
|
|
126153
|
+
started = true;
|
|
126154
|
+
options.topic(topicAbort.signal).then((answer) => {
|
|
126155
|
+
if (settled || !isTopicPress(answer))
|
|
126156
|
+
return;
|
|
126157
|
+
topicAnswer = answer;
|
|
126158
|
+
dockAbort.abort("topic");
|
|
126159
|
+
}).catch(() => {
|
|
126160
|
+
return;
|
|
126161
|
+
});
|
|
126162
|
+
},
|
|
126163
|
+
topicWon: () => topicAnswer !== undefined
|
|
126164
|
+
};
|
|
126165
|
+
try {
|
|
126166
|
+
const result = await options.dock(race);
|
|
126167
|
+
if (topicAnswer !== undefined)
|
|
126168
|
+
return { from: "topic", answer: topicAnswer };
|
|
126169
|
+
return { from: "dock", result };
|
|
126170
|
+
} finally {
|
|
126171
|
+
settled = true;
|
|
126172
|
+
if (topicAnswer === undefined)
|
|
126173
|
+
topicAbort.abort(options.turnSignal.aborted ? "cancelled" : "shell");
|
|
126174
|
+
}
|
|
126175
|
+
}
|
|
126176
|
+
|
|
125794
126177
|
// src/remote/history-host.ts
|
|
125795
126178
|
function sessionEntryOf(resumed) {
|
|
125796
126179
|
return { resumed, kind: resumed ? "resumed" : "new" };
|
|
@@ -157197,9 +157580,9 @@ function createRemoteQueueWiring(deps) {
|
|
|
157197
157580
|
if (bridge?.active !== true)
|
|
157198
157581
|
return;
|
|
157199
157582
|
const closing = bridge.disable();
|
|
157200
|
-
deps.say(`\u25C7 remote control is turning off: the session changed, and its Telegram topic is being
|
|
157583
|
+
deps.say(`\u25C7 remote control is turning off: the session changed, and its Telegram topic is being archived. Turn it on again with /remote-control <name>.
|
|
157201
157584
|
`);
|
|
157202
|
-
closing.then(() => deps.say(`\u25C7 remote control off: the topic is
|
|
157585
|
+
closing.then(() => deps.say(`\u25C7 remote control off: the topic is archived.
|
|
157203
157586
|
`));
|
|
157204
157587
|
},
|
|
157205
157588
|
dropQueuedTelegramLines() {
|
|
@@ -157231,7 +157614,7 @@ function createRemoteQueueWiring(deps) {
|
|
|
157231
157614
|
},
|
|
157232
157615
|
wrapOnToolCall(base) {
|
|
157233
157616
|
return (...args) => {
|
|
157234
|
-
deps.getBridge()?.toolCall();
|
|
157617
|
+
deps.getBridge()?.toolCall(typeof args[0] === "string" ? args[0] : undefined);
|
|
157235
157618
|
base?.(...args);
|
|
157236
157619
|
};
|
|
157237
157620
|
},
|
|
@@ -157513,7 +157896,7 @@ function clip6(s, max) {
|
|
|
157513
157896
|
}
|
|
157514
157897
|
return `${s.slice(0, Math.max(1, max - 1))}\u2026`;
|
|
157515
157898
|
}
|
|
157516
|
-
function
|
|
157899
|
+
function formatElapsed2(ms) {
|
|
157517
157900
|
if (!Number.isFinite(ms) || ms < 0) {
|
|
157518
157901
|
return "\u2014";
|
|
157519
157902
|
}
|
|
@@ -157588,7 +157971,7 @@ function formatSubagentMeta(session, now = Date.now()) {
|
|
|
157588
157971
|
session.runtime === "external" ? `external${session.agentId === undefined ? "" : ` (${session.agentId})`}` : "keryx"
|
|
157589
157972
|
],
|
|
157590
157973
|
["Model", session.model ?? "\u2014"],
|
|
157591
|
-
["Elapsed",
|
|
157974
|
+
["Elapsed", formatElapsed2(end - session.startedAt)],
|
|
157592
157975
|
["Task", session.task ?? "\u2014"]
|
|
157593
157976
|
];
|
|
157594
157977
|
const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0);
|
|
@@ -166189,6 +166572,7 @@ async function launchTuiAgentShell(opts) {
|
|
|
166189
166572
|
jobs.subscribe(paintJobs);
|
|
166190
166573
|
setSubagentFleetListener((ev) => {
|
|
166191
166574
|
sessions.apply(ev);
|
|
166575
|
+
remoteBridge?.subagentEvent(ev);
|
|
166192
166576
|
});
|
|
166193
166577
|
setBackgroundJobListener((ev) => {
|
|
166194
166578
|
jobs.apply(ev);
|
|
@@ -166608,7 +166992,8 @@ async function launchTuiAgentShell(opts) {
|
|
|
166608
166992
|
}));
|
|
166609
166993
|
};
|
|
166610
166994
|
const approvalContext = createApprovalContextLoader(opts.session?.cwd ?? process.cwd());
|
|
166611
|
-
const confirmScheduleCard = async (card) => {
|
|
166995
|
+
const confirmScheduleCard = async (card, race) => {
|
|
166996
|
+
const dockSignal = race?.signal ?? foregroundOperation.signal;
|
|
166612
166997
|
for (const line of card) {
|
|
166613
166998
|
transcript.add(new otui.TextRenderable(r, { id: `ap${uid++}`, content: otui.t`${roleChunk(otui, "attention", line)}` }));
|
|
166614
166999
|
}
|
|
@@ -166618,14 +167003,19 @@ async function launchTuiAgentShell(opts) {
|
|
|
166618
167003
|
title: "Create this schedule and install its background timer?",
|
|
166619
167004
|
subtitle: card[0] ?? "",
|
|
166620
167005
|
cancelId: "cancel",
|
|
166621
|
-
onOpen: () =>
|
|
166622
|
-
|
|
167006
|
+
onOpen: () => {
|
|
167007
|
+
chrome.blurComposer();
|
|
167008
|
+
race?.opened();
|
|
167009
|
+
},
|
|
167010
|
+
signal: dockSignal,
|
|
166623
167011
|
options: [
|
|
166624
167012
|
{ id: "create", label: "Create and install", description: "Store it and install the timer shown above" },
|
|
166625
167013
|
{ id: "cancel", label: "Cancel", description: "Nothing is written or installed" }
|
|
166626
167014
|
]
|
|
166627
167015
|
}));
|
|
166628
167016
|
input.focus();
|
|
167017
|
+
if (race?.topicWon() === true)
|
|
167018
|
+
return false;
|
|
166629
167019
|
setMainAgent("running", id === "create" ? "schedule" : "denied");
|
|
166630
167020
|
transcript.add(new otui.TextRenderable(r, {
|
|
166631
167021
|
id: `ap${uid++}`,
|
|
@@ -166633,7 +167023,8 @@ async function launchTuiAgentShell(opts) {
|
|
|
166633
167023
|
}));
|
|
166634
167024
|
return id === "create";
|
|
166635
167025
|
};
|
|
166636
|
-
const confirmUserPromptCard = async (card) => {
|
|
167026
|
+
const confirmUserPromptCard = async (card, race) => {
|
|
167027
|
+
const dockSignal = race?.signal ?? foregroundOperation.signal;
|
|
166637
167028
|
for (const line of card) {
|
|
166638
167029
|
transcript.add(new otui.TextRenderable(r, { id: `ap${uid++}`, content: otui.t`${roleChunk(otui, "attention", line)}` }));
|
|
166639
167030
|
}
|
|
@@ -166643,14 +167034,19 @@ async function launchTuiAgentShell(opts) {
|
|
|
166643
167034
|
title: "Send this message to the model?",
|
|
166644
167035
|
subtitle: card[0] ?? "",
|
|
166645
167036
|
cancelId: "cancel",
|
|
166646
|
-
onOpen: () =>
|
|
166647
|
-
|
|
167037
|
+
onOpen: () => {
|
|
167038
|
+
chrome.blurComposer();
|
|
167039
|
+
race?.opened();
|
|
167040
|
+
},
|
|
167041
|
+
signal: dockSignal,
|
|
166648
167042
|
options: [
|
|
166649
167043
|
{ id: "send", label: "Send", description: "Let the message reach the model as written" },
|
|
166650
167044
|
{ id: "cancel", label: "Cancel", description: "The message is not sent" }
|
|
166651
167045
|
]
|
|
166652
167046
|
}));
|
|
166653
167047
|
input.focus();
|
|
167048
|
+
if (race?.topicWon() === true)
|
|
167049
|
+
return false;
|
|
166654
167050
|
setMainAgent("running", id === "send" ? "prompt" : "denied");
|
|
166655
167051
|
transcript.add(new otui.TextRenderable(r, {
|
|
166656
167052
|
id: `ap${uid++}`,
|
|
@@ -166682,7 +167078,8 @@ async function launchTuiAgentShell(opts) {
|
|
|
166682
167078
|
content: otui.t`${dimChunk(otui, approvalCauseLine(causeOfMeta(tool, meta)))}`
|
|
166683
167079
|
}));
|
|
166684
167080
|
};
|
|
166685
|
-
const requestApprovalLocally = async (tool, inputJson, meta) => {
|
|
167081
|
+
const requestApprovalLocally = async (tool, inputJson, meta, race) => {
|
|
167082
|
+
const dockSignal = race?.signal ?? foregroundOperation.signal;
|
|
166686
167083
|
if (meta?.untrustedOrigin === true) {
|
|
166687
167084
|
transcript.add(new otui.TextRenderable(r, {
|
|
166688
167085
|
id: `ap${uid++}`,
|
|
@@ -166691,11 +167088,11 @@ async function launchTuiAgentShell(opts) {
|
|
|
166691
167088
|
}
|
|
166692
167089
|
if (isUserPromptApprovalCard(tool) && meta?.alwaysAsk === true && meta.card !== undefined) {
|
|
166693
167090
|
addCauseLine(tool, meta);
|
|
166694
|
-
return confirmUserPromptCard(meta.card);
|
|
167091
|
+
return confirmUserPromptCard(meta.card, race);
|
|
166695
167092
|
}
|
|
166696
167093
|
if (meta?.alwaysAsk === true && meta.card !== undefined) {
|
|
166697
167094
|
addCauseLine(tool, meta);
|
|
166698
|
-
return confirmScheduleCard(meta.card);
|
|
167095
|
+
return confirmScheduleCard(meta.card, race);
|
|
166699
167096
|
}
|
|
166700
167097
|
if (tool === "spawn_subagent") {
|
|
166701
167098
|
let mode = "read_only";
|
|
@@ -166726,8 +167123,11 @@ async function launchTuiAgentShell(opts) {
|
|
|
166726
167123
|
title: mode === "read_only" ? meta?.hookAsk === true ? "Spawn read-only subagent? (policy hook asked)" : "Spawn read-only subagent? (follows untrusted content)" : "Spawn general subagent?",
|
|
166727
167124
|
subtitle: taskPreview,
|
|
166728
167125
|
cancelId: "deny",
|
|
166729
|
-
onOpen: () =>
|
|
166730
|
-
|
|
167126
|
+
onOpen: () => {
|
|
167127
|
+
chrome.blurComposer();
|
|
167128
|
+
race?.opened();
|
|
167129
|
+
},
|
|
167130
|
+
signal: dockSignal,
|
|
166731
167131
|
options: [
|
|
166732
167132
|
{
|
|
166733
167133
|
id: "allow",
|
|
@@ -166739,6 +167139,8 @@ async function launchTuiAgentShell(opts) {
|
|
|
166739
167139
|
]
|
|
166740
167140
|
}));
|
|
166741
167141
|
input.focus();
|
|
167142
|
+
if (race?.topicWon() === true)
|
|
167143
|
+
return false;
|
|
166742
167144
|
setMainAgent("running", id === "allow" ? "subagent" : "denied");
|
|
166743
167145
|
transcript.add(new otui.TextRenderable(r, {
|
|
166744
167146
|
id: `ap${uid++}`,
|
|
@@ -166771,14 +167173,19 @@ async function launchTuiAgentShell(opts) {
|
|
|
166771
167173
|
title: "Approve apply_patch?",
|
|
166772
167174
|
subtitle: "Diff shown above \xB7 Esc denies",
|
|
166773
167175
|
cancelId: "deny",
|
|
166774
|
-
onOpen: () =>
|
|
166775
|
-
|
|
167176
|
+
onOpen: () => {
|
|
167177
|
+
chrome.blurComposer();
|
|
167178
|
+
race?.opened();
|
|
167179
|
+
},
|
|
167180
|
+
signal: dockSignal,
|
|
166776
167181
|
options: [
|
|
166777
167182
|
{ id: "allow", label: "Approve", description: "Write the patch to disk", recommended: true },
|
|
166778
167183
|
{ id: "deny", label: "Deny", description: "Do not write anything" }
|
|
166779
167184
|
]
|
|
166780
167185
|
}));
|
|
166781
167186
|
input.focus();
|
|
167187
|
+
if (race?.topicWon() === true)
|
|
167188
|
+
return false;
|
|
166782
167189
|
setMainAgent("running", id === "allow" ? "write" : "denied");
|
|
166783
167190
|
transcript.add(new otui.TextRenderable(r, {
|
|
166784
167191
|
id: `ap${uid++}`,
|
|
@@ -166813,8 +167220,11 @@ async function launchTuiAgentShell(opts) {
|
|
|
166813
167220
|
title: described.title,
|
|
166814
167221
|
subtitle: summariseUseToolApproval(described),
|
|
166815
167222
|
cancelId: "deny",
|
|
166816
|
-
onOpen: () =>
|
|
166817
|
-
|
|
167223
|
+
onOpen: () => {
|
|
167224
|
+
chrome.blurComposer();
|
|
167225
|
+
race?.opened();
|
|
167226
|
+
},
|
|
167227
|
+
signal: dockSignal,
|
|
166818
167228
|
options: [
|
|
166819
167229
|
{ id: APPROVAL_ALLOW_ID, label: "Approve", description: `run ${described.tool} on ${described.server}` },
|
|
166820
167230
|
...canTrustMcp ? [{ id: "trust-mcp", label: "Trust this tool (this session)", description: `Skip future approvals for ${described.fqn} in trust mode` }] : [],
|
|
@@ -166822,6 +167232,8 @@ async function launchTuiAgentShell(opts) {
|
|
|
166822
167232
|
]
|
|
166823
167233
|
}));
|
|
166824
167234
|
input.focus();
|
|
167235
|
+
if (race?.topicWon() === true)
|
|
167236
|
+
return false;
|
|
166825
167237
|
const verdict = mcpDockVerdict(id, canTrustMcp, meta?.fingerprint);
|
|
166826
167238
|
const allowed = verdict !== false;
|
|
166827
167239
|
setMainAgent("running", allowed ? "write" : "denied");
|
|
@@ -166864,14 +167276,19 @@ async function launchTuiAgentShell(opts) {
|
|
|
166864
167276
|
title: "Approve codex elicitation?",
|
|
166865
167277
|
subtitle: elicitationSubtitle,
|
|
166866
167278
|
cancelId: "deny",
|
|
166867
|
-
onOpen: () =>
|
|
166868
|
-
|
|
167279
|
+
onOpen: () => {
|
|
167280
|
+
chrome.blurComposer();
|
|
167281
|
+
race?.opened();
|
|
167282
|
+
},
|
|
167283
|
+
signal: dockSignal,
|
|
166869
167284
|
options: [
|
|
166870
167285
|
{ id: "allow", label: "Approve", description: "codex proceeds with this action", recommended: true },
|
|
166871
167286
|
{ id: "deny", label: "Deny", description: "codex's action is refused" }
|
|
166872
167287
|
]
|
|
166873
167288
|
}));
|
|
166874
167289
|
input.focus();
|
|
167290
|
+
if (race?.topicWon() === true)
|
|
167291
|
+
return false;
|
|
166875
167292
|
setMainAgent("running", id === "allow" ? "write" : "denied");
|
|
166876
167293
|
transcript.add(new otui.TextRenderable(r, {
|
|
166877
167294
|
id: `ap${uid++}`,
|
|
@@ -166927,10 +167344,15 @@ async function launchTuiAgentShell(opts) {
|
|
|
166927
167344
|
chrome.hideMenu();
|
|
166928
167345
|
addCauseLine(tool, meta);
|
|
166929
167346
|
const choice = await chrome.withOverlay(() => pickShellApproval(otui, r, chrome.dock, cmd, approvalContext, destructive, meta?.credentials === true, {
|
|
166930
|
-
onOpen: () =>
|
|
166931
|
-
|
|
167347
|
+
onOpen: () => {
|
|
167348
|
+
chrome.blurComposer();
|
|
167349
|
+
race?.opened();
|
|
167350
|
+
},
|
|
167351
|
+
signal: dockSignal
|
|
166932
167352
|
}, ev.publishLease, ev.publishLeaseDetail, ev.hookAsk || ev.untrustedOrigin, causeOfMeta(tool, meta)));
|
|
166933
167353
|
input.focus();
|
|
167354
|
+
if (race?.topicWon() === true)
|
|
167355
|
+
return false;
|
|
166934
167356
|
if (choice === "deny") {
|
|
166935
167357
|
transcript.add(new otui.TextRenderable(r, {
|
|
166936
167358
|
id: `av${uid++}`,
|
|
@@ -167017,7 +167439,30 @@ async function launchTuiAgentShell(opts) {
|
|
|
167017
167439
|
return false;
|
|
167018
167440
|
return approvedAnswer();
|
|
167019
167441
|
};
|
|
167020
|
-
|
|
167442
|
+
const mirrorApprovalToTopic = (inner) => async (tool, inputJson, meta) => {
|
|
167443
|
+
const bridge = remoteBridge;
|
|
167444
|
+
if (bridge === undefined || !bridge.active) {
|
|
167445
|
+
return inner(tool, inputJson, meta);
|
|
167446
|
+
}
|
|
167447
|
+
const brief = tool.length > 60 ? `${tool.slice(0, 59)}\u2026` : tool;
|
|
167448
|
+
const outcome = await raceApproval({
|
|
167449
|
+
turnSignal: foregroundOperation.signal,
|
|
167450
|
+
dock: (race) => inner(tool, inputJson, meta, race),
|
|
167451
|
+
topic: (signal) => bridge.askApproval(describeApprovalForTopic(tool, inputJson, meta), { signal, timeoutMs: MAX_APPROVAL_TIMEOUT_MS })
|
|
167452
|
+
});
|
|
167453
|
+
if (outcome.from === "dock")
|
|
167454
|
+
return outcome.result;
|
|
167455
|
+
const allowed = outcome.answer.decision === "allow";
|
|
167456
|
+
input.focus();
|
|
167457
|
+
setMainAgent("running", allowed ? "approved (telegram)" : "denied");
|
|
167458
|
+
io.onSystem?.(allowed ? `\u25C7 ${brief} approved in Telegram.
|
|
167459
|
+
` : `\u25C7 ${brief} denied in Telegram.
|
|
167460
|
+
`);
|
|
167461
|
+
if (!allowed)
|
|
167462
|
+
return false;
|
|
167463
|
+
return meta?.fingerprint !== undefined ? { approved: true, fingerprint: meta.fingerprint } : true;
|
|
167464
|
+
};
|
|
167465
|
+
io.requestApproval = trackApproval(mirrorApprovalToTopic(requestApprovalLocally));
|
|
167021
167466
|
const askUserInteractive = async (req) => {
|
|
167022
167467
|
chrome.hideMenu();
|
|
167023
167468
|
setMainAgent("blocked", "ask");
|
|
@@ -167027,7 +167472,7 @@ async function launchTuiAgentShell(opts) {
|
|
|
167027
167472
|
id: `ask${uid++}`,
|
|
167028
167473
|
content: otui.t`${roleChunk(otui, "attention", "? ")} ${dimChunk(otui, qShort)}`
|
|
167029
167474
|
}));
|
|
167030
|
-
const topicBridge = remoteBridge?.
|
|
167475
|
+
const topicBridge = remoteBridge?.active === true ? remoteBridge : undefined;
|
|
167031
167476
|
const outcome = await raceAskUser({
|
|
167032
167477
|
turnSignal: foregroundOperation.signal,
|
|
167033
167478
|
...topicBridge === undefined ? {} : { topic: (signal) => topicBridge.askUser(req, signal) },
|
|
@@ -170182,6 +170627,7 @@ Turn: ${chrome.isBusy() ? "running" : "idle"} \xB7 queue: ${mainQueue.length} wa
|
|
|
170182
170627
|
project: () => sessionCwd,
|
|
170183
170628
|
isBusy: () => chrome.isBusy() || foregroundOperation.isActive || forceHandoff.isAwaitingSettlement || mainQueue.length > 0 || leaseView()?.held() === true,
|
|
170184
170629
|
turnRunning: () => chrome.isBusy() || foregroundOperation.isActive,
|
|
170630
|
+
sessionState: () => ({ queued: mainQueue.length, mode: formatModeInForce(modeForTurn(true)) }),
|
|
170185
170631
|
runLine: (text) => remoteQueue.runTelegramLine(text),
|
|
170186
170632
|
enqueue: (text) => remoteQueue.enqueueTelegramLine(text),
|
|
170187
170633
|
notice: (text) => io.onSystem?.(`${text}
|
|
@@ -170341,6 +170787,7 @@ var init_tui_shell = __esm(() => {
|
|
|
170341
170787
|
init_permissions_inspector();
|
|
170342
170788
|
init_approval_cause();
|
|
170343
170789
|
init_shell_bridge();
|
|
170790
|
+
init_protocol2();
|
|
170344
170791
|
init_command_gateway();
|
|
170345
170792
|
init_history_host();
|
|
170346
170793
|
init_channels_client();
|
|
@@ -210019,6 +210466,12 @@ function createHttpBotApi(options) {
|
|
|
210019
210466
|
message_thread_id: params.messageThreadId
|
|
210020
210467
|
});
|
|
210021
210468
|
},
|
|
210469
|
+
async closeForumTopic(params) {
|
|
210470
|
+
await call("closeForumTopic", {
|
|
210471
|
+
chat_id: params.chatId,
|
|
210472
|
+
message_thread_id: params.messageThreadId
|
|
210473
|
+
});
|
|
210474
|
+
},
|
|
210022
210475
|
async editForumTopic(params) {
|
|
210023
210476
|
await call("editForumTopic", {
|
|
210024
210477
|
chat_id: params.chatId,
|
|
@@ -212585,7 +213038,7 @@ class RemoteHub {
|
|
|
212585
213038
|
if (record === undefined) {
|
|
212586
213039
|
return { ok: true, existed: false };
|
|
212587
213040
|
}
|
|
212588
|
-
await this.
|
|
213041
|
+
await this.archive(record, "The session was closed.");
|
|
212589
213042
|
return { ok: true, existed: true };
|
|
212590
213043
|
});
|
|
212591
213044
|
}
|
|
@@ -212644,11 +213097,11 @@ class RemoteHub {
|
|
|
212644
213097
|
this.event("session-unavailable", record.name);
|
|
212645
213098
|
this.endActivity(record.sessionId);
|
|
212646
213099
|
if (now - record.unavailableSince < this.config.orphanMs) {
|
|
212647
|
-
this.queueStatus(record, `Session unavailable: the shell stopped responding. This topic is
|
|
213100
|
+
this.queueStatus(record, `Session unavailable: the shell stopped responding. This topic is archived in ${formatDuration(this.config.orphanMs)} unless the session returns.`);
|
|
212648
213101
|
}
|
|
212649
213102
|
}
|
|
212650
213103
|
if (record.status === "unavailable" && record.unavailableSince !== undefined && now - record.unavailableSince >= this.config.orphanMs) {
|
|
212651
|
-
await this.
|
|
213104
|
+
await this.archive(record, "The shell stopped responding and did not return.");
|
|
212652
213105
|
}
|
|
212653
213106
|
}
|
|
212654
213107
|
await this.flushOutbound();
|
|
@@ -212912,6 +213365,7 @@ class RemoteHub {
|
|
|
212912
213365
|
markLive(record) {
|
|
212913
213366
|
record.status = "live";
|
|
212914
213367
|
delete record.unavailableSince;
|
|
213368
|
+
delete record.closing;
|
|
212915
213369
|
}
|
|
212916
213370
|
queueStatus(record, text) {
|
|
212917
213371
|
this.outbound.enqueue({ chatId: record.chatId, threadId: record.threadId, text });
|
|
@@ -212991,6 +213445,31 @@ class RemoteHub {
|
|
|
212991
213445
|
await this.flushOutbound();
|
|
212992
213446
|
return { ok: true, name, threadId: record.threadId, reused: false };
|
|
212993
213447
|
}
|
|
213448
|
+
async archive(record, reason) {
|
|
213449
|
+
this.stopTyping(record);
|
|
213450
|
+
if (record.closing !== true) {
|
|
213451
|
+
record.closing = true;
|
|
213452
|
+
this.queueStatus(record, `${reason} This topic is archived: the history stays here and nothing more will be posted. A new session opens a new topic.`);
|
|
213453
|
+
this.registry.save();
|
|
213454
|
+
}
|
|
213455
|
+
await this.flushOutbound();
|
|
213456
|
+
try {
|
|
213457
|
+
await this.api.closeForumTopic({ chatId: record.chatId, messageThreadId: record.threadId });
|
|
213458
|
+
} catch (error) {
|
|
213459
|
+
if (isBotApiError(error) && isRetryable(error)) {
|
|
213460
|
+
record.sessionId = "";
|
|
213461
|
+
record.status = "unavailable";
|
|
213462
|
+
record.unavailableSince = this.now() - this.config.orphanMs;
|
|
213463
|
+
this.registry.save();
|
|
213464
|
+
this.event("topic-close-failed", `${record.name}: ${describeError4(error)}`);
|
|
213465
|
+
return;
|
|
213466
|
+
}
|
|
213467
|
+
}
|
|
213468
|
+
this.inbound.remove(nameKey(record.name));
|
|
213469
|
+
this.registry.remove(record.name);
|
|
213470
|
+
this.registry.save();
|
|
213471
|
+
this.event("topic-archived", `${record.name}: ${reason}`);
|
|
213472
|
+
}
|
|
212994
213473
|
async retire(record, why, strict = false) {
|
|
212995
213474
|
this.outbound.discardForThread(record.chatId, record.threadId);
|
|
212996
213475
|
this.stopTyping(record);
|
|
@@ -213463,6 +213942,7 @@ class RemoteHttpSurface {
|
|
|
213463
213942
|
keepaliveMs;
|
|
213464
213943
|
ackTimeoutMs;
|
|
213465
213944
|
approvalAckMs;
|
|
213945
|
+
reminderMs;
|
|
213466
213946
|
newApprovalId;
|
|
213467
213947
|
newPromptId;
|
|
213468
213948
|
streams = new Map;
|
|
@@ -213483,6 +213963,7 @@ class RemoteHttpSurface {
|
|
|
213483
213963
|
this.keepaliveMs = options.keepaliveMs ?? DEFAULT_KEEPALIVE_MS;
|
|
213484
213964
|
this.ackTimeoutMs = options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS;
|
|
213485
213965
|
this.approvalAckMs = options.approvalAckMs ?? DEFAULT_APPROVAL_ACK_MS;
|
|
213966
|
+
this.reminderMs = options.reminderMs ?? DEFAULT_PROMPT_REMINDER_MS;
|
|
213486
213967
|
this.newApprovalId = options.approvalIds ?? (() => `ap${randomBytes16(6).toString("hex")}`);
|
|
213487
213968
|
this.newPromptId = options.promptIds ?? (() => `pk${randomBytes16(6).toString("hex")}`);
|
|
213488
213969
|
this.consumer = {
|
|
@@ -213779,6 +214260,7 @@ class RemoteHttpSurface {
|
|
|
213779
214260
|
timer: this.timers.setTimeout(() => this.expireApproval(approvalId), timeoutMs),
|
|
213780
214261
|
...remember === undefined ? {} : { remember }
|
|
213781
214262
|
});
|
|
214263
|
+
this.scheduleReminder("approval", approvalId);
|
|
213782
214264
|
const offer = remember === undefined ? "" : `
|
|
213783
214265
|
Always would remember:
|
|
213784
214266
|
${asCodeBlock(remember)}`;
|
|
@@ -213908,6 +214390,7 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}${offer}`, {
|
|
|
213908
214390
|
...own ? { own: true } : {},
|
|
213909
214391
|
timer: this.timers.setTimeout(() => this.expireChoice(promptId), timeoutMs)
|
|
213910
214392
|
});
|
|
214393
|
+
this.scheduleReminder("question", promptId);
|
|
213911
214394
|
const keyboard = [];
|
|
213912
214395
|
let position = 0;
|
|
213913
214396
|
for (const width of layout) {
|
|
@@ -213940,12 +214423,15 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}${offer}`, {
|
|
|
213940
214423
|
if (!isSessionId(value.sessionId)) {
|
|
213941
214424
|
return invalid2("sessionId must be 1 to 64 characters of letters, digits, '-' and '_'.");
|
|
213942
214425
|
}
|
|
213943
|
-
if (typeof value.promptId !== "string" || !isPromptId(value.promptId)) {
|
|
213944
|
-
return invalid2("promptId must be a prompt id this serve issued.");
|
|
214426
|
+
if (typeof value.promptId !== "string" || !(isPromptId(value.promptId) || isApprovalId(value.promptId))) {
|
|
214427
|
+
return invalid2("promptId must be a prompt or approval id this serve issued.");
|
|
213945
214428
|
}
|
|
213946
214429
|
if (value.by !== undefined && value.by !== "shell" && value.by !== "cancelled") {
|
|
213947
214430
|
return invalid2("by must be 'shell' or 'cancelled'.");
|
|
213948
214431
|
}
|
|
214432
|
+
if (isApprovalId(value.promptId)) {
|
|
214433
|
+
return this.closeApproval(value.promptId, value.sessionId, value.by);
|
|
214434
|
+
}
|
|
213949
214435
|
const entry = this.choices.get(value.promptId);
|
|
213950
214436
|
if (entry === undefined || entry.sessionId !== value.sessionId) {
|
|
213951
214437
|
const done = this.finished.get(value.promptId);
|
|
@@ -213967,6 +214453,21 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}${offer}`, {
|
|
|
213967
214453
|
});
|
|
213968
214454
|
return ok2({ closed: true });
|
|
213969
214455
|
}
|
|
214456
|
+
closeApproval(approvalId, sessionId, by) {
|
|
214457
|
+
const entry = this.approvals.get(approvalId);
|
|
214458
|
+
if (entry === undefined || entry.sessionId !== sessionId) {
|
|
214459
|
+
return ok2({ closed: false });
|
|
214460
|
+
}
|
|
214461
|
+
this.dropApproval(approvalId);
|
|
214462
|
+
this.rememberDecided(approvalId, sessionId);
|
|
214463
|
+
const when = clockText(this.now());
|
|
214464
|
+
const answered = by !== "cancelled";
|
|
214465
|
+
this.finishApproval(approvalId, sessionId, entry.messageId, entry.text, {
|
|
214466
|
+
result: answered ? `Answered in the shell at ${when}.` : `Cancelled at ${when}: the request is no longer open.`,
|
|
214467
|
+
shortReply: answered ? "Answered in the shell." : "That approval is no longer open."
|
|
214468
|
+
});
|
|
214469
|
+
return ok2({ closed: true });
|
|
214470
|
+
}
|
|
213970
214471
|
async messageState(hub, request) {
|
|
213971
214472
|
const body = await readJsonBody(request);
|
|
213972
214473
|
if (!body.ok) {
|
|
@@ -214415,6 +214916,7 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}${offer}`, {
|
|
|
214415
214916
|
return;
|
|
214416
214917
|
}
|
|
214417
214918
|
this.timers.clearTimeout(entry.timer);
|
|
214919
|
+
this.timers.clearTimeout(entry.reminder);
|
|
214418
214920
|
this.choices.delete(promptId);
|
|
214419
214921
|
if (entry.armedReplyId !== undefined) {
|
|
214420
214922
|
const armed = this.armedOwn.get(entry.armedReplyId);
|
|
@@ -214491,6 +214993,7 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}${offer}`, {
|
|
|
214491
214993
|
return;
|
|
214492
214994
|
}
|
|
214493
214995
|
this.timers.clearTimeout(entry.timer);
|
|
214996
|
+
this.timers.clearTimeout(entry.reminder);
|
|
214494
214997
|
this.approvals.delete(approvalId);
|
|
214495
214998
|
}
|
|
214496
214999
|
dropAwaiting(approvalId) {
|
|
@@ -214524,12 +215027,36 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}${offer}`, {
|
|
|
214524
215027
|
shortReply: "Sent to the shell, not confirmed; it denies by itself if it did not receive it."
|
|
214525
215028
|
});
|
|
214526
215029
|
}
|
|
215030
|
+
scheduleReminder(kind, id) {
|
|
215031
|
+
const entry = kind === "approval" ? this.approvals.get(id) : this.choices.get(id);
|
|
215032
|
+
if (entry === undefined || (entry.reminded ?? 0) >= MAX_PROMPT_REMINDERS || this.reminderMs <= 0) {
|
|
215033
|
+
return;
|
|
215034
|
+
}
|
|
215035
|
+
entry.reminder = this.timers.setTimeout(() => this.remind(kind, id), this.reminderMs);
|
|
215036
|
+
}
|
|
215037
|
+
remind(kind, id) {
|
|
215038
|
+
const entry = kind === "approval" ? this.approvals.get(id) : this.choices.get(id);
|
|
215039
|
+
const hub = this.hub;
|
|
215040
|
+
if (entry === undefined || hub === undefined || this.closed) {
|
|
215041
|
+
return;
|
|
215042
|
+
}
|
|
215043
|
+
entry.reminded = (entry.reminded ?? 0) + 1;
|
|
215044
|
+
const left = entry.expiresAt - this.now();
|
|
215045
|
+
const what = kind === "approval" ? "An approval is" : "A question is";
|
|
215046
|
+
const topic = reminderExcerpt(entry.text);
|
|
215047
|
+
const ends = left > 0 ? ` It expires at ${clockText(entry.expiresAt)}.` : "";
|
|
215048
|
+
hub.send(entry.sessionId, `Reminder: ${what} still waiting for an answer here${topic === "" ? "" : `: ${topic}`}.${ends} The buttons are on the message above.`).catch(() => {
|
|
215049
|
+
return;
|
|
215050
|
+
});
|
|
215051
|
+
this.scheduleReminder(kind, id);
|
|
215052
|
+
}
|
|
214527
215053
|
expireApproval(approvalId) {
|
|
214528
215054
|
const entry = this.approvals.get(approvalId);
|
|
214529
215055
|
if (entry === undefined) {
|
|
214530
215056
|
return;
|
|
214531
215057
|
}
|
|
214532
215058
|
this.timers.clearTimeout(entry.timer);
|
|
215059
|
+
this.timers.clearTimeout(entry.reminder);
|
|
214533
215060
|
this.approvals.delete(approvalId);
|
|
214534
215061
|
this.finishApproval(approvalId, entry.sessionId, entry.messageId, entry.text, {
|
|
214535
215062
|
result: `Expired at ${clockText(this.now())}: no answer, so it was denied.`,
|
|
@@ -214581,6 +215108,12 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}${offer}`, {
|
|
|
214581
215108
|
}
|
|
214582
215109
|
}
|
|
214583
215110
|
}
|
|
215111
|
+
function reminderExcerpt(text) {
|
|
215112
|
+
const line = (text ?? "").split(`
|
|
215113
|
+
`).map((part) => part.replace(/^`{3,}.*$/, "").replace(/^Approval needed:\s*/, "").trim()).find((part) => part.length > 0) ?? "";
|
|
215114
|
+
const flat = line.replace(/\s+/g, " ");
|
|
215115
|
+
return flat.length <= 80 ? flat : `${flat.slice(0, 79)}\u2026`;
|
|
215116
|
+
}
|
|
214584
215117
|
function alwaysButtonText(pattern) {
|
|
214585
215118
|
const text = `Always: ${pattern.replace(/\s+/g, " ")}`;
|
|
214586
215119
|
return text.length <= MAX_BUTTON_TEXT_CHARS ? text : `${text.slice(0, MAX_BUTTON_TEXT_CHARS - 1)}\u2026`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.111",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|