@mrciphersmith/keryx 0.3.66 → 0.3.67
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 +10 -2
- package/dist/cli.js +387 -96
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1053,8 +1053,16 @@ once to HTML and once to plain text, so a reply is never dropped. `keryx remote
|
|
|
1053
1053
|
format-sample` prints the sample in every mode with no network, and `/channels`
|
|
1054
1054
|
shows the mode and the last fallback; see
|
|
1055
1055
|
[how replies look in Telegram](docs/docs/guides/drive-keryx-remotely.md#how-replies-look-in-telegram).
|
|
1056
|
-
|
|
1057
|
-
|
|
1056
|
+
An approval question is confirmed by the shell: after you press Allow or Deny,
|
|
1057
|
+
`serve` waits five seconds for the shell to say it applied the answer to the question
|
|
1058
|
+
it was waiting on, then edits the message to "Allowed by ... at ..." or "Denied by ... at ...".
|
|
1059
|
+
If the shell was no longer waiting (the question had timed out or the session had
|
|
1060
|
+
reconnected) the message says "Not applied" and that the answer changed nothing. If the shell does
|
|
1061
|
+
not answer in time the message says the answer was sent to the shell, **not
|
|
1062
|
+
confirmed**: the shell did not say in time that it received the answer (a shell that never got an Allow lets its own question time out as a denial; one that got it late still applies it); while
|
|
1063
|
+
that is so the shell shows an "Approvals not confirmed" count in `/remote-control`
|
|
1064
|
+
and in the sidebar row. This has been verified against a fake Bot API only; no run
|
|
1065
|
+
against real Telegram has been done yet. Setup and limits:
|
|
1058
1066
|
[drive keryx remotely](docs/docs/guides/drive-keryx-remotely.md#remote-control-from-telegram).
|
|
1059
1067
|
|
|
1060
1068
|
## CI integration
|
package/dist/cli.js
CHANGED
|
@@ -20107,7 +20107,7 @@ var package_default;
|
|
|
20107
20107
|
var init_package = __esm(() => {
|
|
20108
20108
|
package_default = {
|
|
20109
20109
|
name: "@mrciphersmith/keryx",
|
|
20110
|
-
version: "0.3.
|
|
20110
|
+
version: "0.3.67",
|
|
20111
20111
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
20112
20112
|
private: false,
|
|
20113
20113
|
publishConfig: {
|
|
@@ -114789,6 +114789,9 @@ function channelsRoutePath(route) {
|
|
|
114789
114789
|
function isSessionId(value) {
|
|
114790
114790
|
return typeof value === "string" && SESSION_ID_PATTERN.test(value);
|
|
114791
114791
|
}
|
|
114792
|
+
function isApprovalId(value) {
|
|
114793
|
+
return typeof value === "string" && APPROVAL_ID_PATTERN.test(value);
|
|
114794
|
+
}
|
|
114792
114795
|
function approvalCallbackData(approvalId, decision) {
|
|
114793
114796
|
return `ap:${approvalId}:${decision}`;
|
|
114794
114797
|
}
|
|
@@ -114861,7 +114864,7 @@ class SseParser {
|
|
|
114861
114864
|
return frames;
|
|
114862
114865
|
}
|
|
114863
114866
|
}
|
|
114864
|
-
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_KEEPALIVE_MS = 15000, DEFAULT_ACK_TIMEOUT_MS = 30000, SESSION_ID_PATTERN, APPROVAL_CALLBACK_PATTERN, RESERVED_CALLBACK_PREFIX = "ap:", CHOICE_CALLBACK_PATTERN, RESERVED_CHOICE_PREFIX = "pk:", MAX_PROMPT_BUTTONS = 24, MESSAGE_STATES, SSE_KEEPALIVE_FRAME = `: keepalive
|
|
114867
|
+
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:", CHOICE_CALLBACK_PATTERN, RESERVED_CHOICE_PREFIX = "pk:", MAX_PROMPT_BUTTONS = 24, MESSAGE_STATES, SSE_KEEPALIVE_FRAME = `: keepalive
|
|
114865
114868
|
|
|
114866
114869
|
`;
|
|
114867
114870
|
var init_protocol2 = __esm(() => {
|
|
@@ -114874,6 +114877,7 @@ var init_protocol2 = __esm(() => {
|
|
|
114874
114877
|
prompt: "POST",
|
|
114875
114878
|
state: "POST",
|
|
114876
114879
|
ack: "POST",
|
|
114880
|
+
"approval-ack": "POST",
|
|
114877
114881
|
stream: "GET"
|
|
114878
114882
|
};
|
|
114879
114883
|
CHANNELS_ROUTE_METHODS = {
|
|
@@ -114889,6 +114893,7 @@ var init_protocol2 = __esm(() => {
|
|
|
114889
114893
|
DEFAULT_APPROVAL_TIMEOUT_MS = 5 * 60000;
|
|
114890
114894
|
MAX_APPROVAL_TIMEOUT_MS = 60 * 60000;
|
|
114891
114895
|
SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
114896
|
+
APPROVAL_ID_PATTERN = /^ap[0-9a-f]{12}$/;
|
|
114892
114897
|
APPROVAL_CALLBACK_PATTERN = /^ap:(ap[0-9a-f]{12}):(allow|deny)$/;
|
|
114893
114898
|
CHOICE_CALLBACK_PATTERN = /^pk:(pk[0-9a-f]{12}):(\d{1,3})$/;
|
|
114894
114899
|
MESSAGE_STATES = ["reading", "working", "done", "failed"];
|
|
@@ -115253,6 +115258,10 @@ class RemoteClient {
|
|
|
115253
115258
|
inboundChain = Promise.resolve();
|
|
115254
115259
|
waiters = new Map;
|
|
115255
115260
|
earlyDecisions = new Map;
|
|
115261
|
+
seenApprovals = new Map;
|
|
115262
|
+
earlyDecisionMs;
|
|
115263
|
+
unackedApprovals = new Map;
|
|
115264
|
+
lingerTimers = new Set;
|
|
115256
115265
|
choiceWaiters = new Map;
|
|
115257
115266
|
earlyChoices = new Map;
|
|
115258
115267
|
firstResult;
|
|
@@ -115269,10 +115278,20 @@ class RemoteClient {
|
|
|
115269
115278
|
this.sleep = options.sleep ?? abortableSleep2;
|
|
115270
115279
|
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
115271
115280
|
this.isAlive = options.isAlive ?? ownProcessIsAlive;
|
|
115281
|
+
this.earlyDecisionMs = options.earlyDecisionMs ?? DEFAULT_EARLY_DECISION_MS;
|
|
115272
115282
|
}
|
|
115273
115283
|
get connected() {
|
|
115274
115284
|
return this.streamOpen && !this.stopped;
|
|
115275
115285
|
}
|
|
115286
|
+
get unconfirmedApprovals() {
|
|
115287
|
+
const cutoff = Date.now() - UNCONFIRMED_APPROVAL_LINGER_MS;
|
|
115288
|
+
for (const [id, at] of [...this.unackedApprovals.entries()]) {
|
|
115289
|
+
if (at < cutoff) {
|
|
115290
|
+
this.unackedApprovals.delete(id);
|
|
115291
|
+
}
|
|
115292
|
+
}
|
|
115293
|
+
return this.unackedApprovals.size;
|
|
115294
|
+
}
|
|
115276
115295
|
start() {
|
|
115277
115296
|
if (this.started) {
|
|
115278
115297
|
return Promise.resolve({ ok: false, code: "already-started", message: "this client was already started", retrying: false });
|
|
@@ -115323,6 +115342,10 @@ class RemoteClient {
|
|
|
115323
115342
|
}
|
|
115324
115343
|
this.lifetime.abort();
|
|
115325
115344
|
this.streamAbort?.abort();
|
|
115345
|
+
for (const timer of this.lingerTimers) {
|
|
115346
|
+
clearTimeout(timer);
|
|
115347
|
+
}
|
|
115348
|
+
this.lingerTimers.clear();
|
|
115326
115349
|
this.failApprovals();
|
|
115327
115350
|
this.firstResult?.({ ok: false, code: "stopped", message: "the client was stopped before it connected", retrying: false });
|
|
115328
115351
|
this.firstResult = undefined;
|
|
@@ -115356,8 +115379,10 @@ class RemoteClient {
|
|
|
115356
115379
|
}
|
|
115357
115380
|
const early = this.earlyDecisions.get(approvalId);
|
|
115358
115381
|
if (early !== undefined) {
|
|
115382
|
+
clearTimeout(early.timer);
|
|
115359
115383
|
this.earlyDecisions.delete(approvalId);
|
|
115360
|
-
|
|
115384
|
+
this.settleApproval(approvalId, early.decision, true);
|
|
115385
|
+
return early.decision;
|
|
115361
115386
|
}
|
|
115362
115387
|
if (!this.connected) {
|
|
115363
115388
|
return "deny";
|
|
@@ -115599,8 +115624,7 @@ class RemoteClient {
|
|
|
115599
115624
|
return;
|
|
115600
115625
|
});
|
|
115601
115626
|
} else if (event === "approval") {
|
|
115602
|
-
|
|
115603
|
-
this.resolveApproval(approval.approvalId, approval.decision);
|
|
115627
|
+
this.handleApprovalFrame(parsed);
|
|
115604
115628
|
} else if (event === "choice") {
|
|
115605
115629
|
const choice = parsed;
|
|
115606
115630
|
this.resolveChoice(choice.promptId, choice.index);
|
|
@@ -115645,16 +115669,115 @@ class RemoteClient {
|
|
|
115645
115669
|
await this.post("ack", { sessionId: this.options.sessionId, updateId });
|
|
115646
115670
|
} catch {}
|
|
115647
115671
|
}
|
|
115672
|
+
handleApprovalFrame(approval) {
|
|
115673
|
+
const approvalId = approval?.approvalId;
|
|
115674
|
+
if (!isApprovalId(approvalId) || approval.decision !== "allow" && approval.decision !== "deny") {
|
|
115675
|
+
return;
|
|
115676
|
+
}
|
|
115677
|
+
const decision = approval.decision;
|
|
115678
|
+
const seen = this.seenApprovals.get(approvalId);
|
|
115679
|
+
if (seen === "pending") {
|
|
115680
|
+
return;
|
|
115681
|
+
}
|
|
115682
|
+
this.countUnconfirmed(approvalId);
|
|
115683
|
+
if (seen !== undefined) {
|
|
115684
|
+
this.ackApproval(approvalId, seen === "applied");
|
|
115685
|
+
return;
|
|
115686
|
+
}
|
|
115687
|
+
if (this.waiters.has(approvalId)) {
|
|
115688
|
+
this.settleApproval(approvalId, decision, true, () => this.resolveApproval(approvalId, decision));
|
|
115689
|
+
return;
|
|
115690
|
+
}
|
|
115691
|
+
this.rememberSeen(approvalId, "pending");
|
|
115692
|
+
this.parkEarlyDecision(approvalId, decision);
|
|
115693
|
+
}
|
|
115694
|
+
rememberSeen(approvalId, outcome) {
|
|
115695
|
+
this.seenApprovals.delete(approvalId);
|
|
115696
|
+
this.seenApprovals.set(approvalId, outcome);
|
|
115697
|
+
if (this.seenApprovals.size > MAX_SEEN_APPROVALS) {
|
|
115698
|
+
const oldest = this.seenApprovals.keys().next().value;
|
|
115699
|
+
if (oldest !== undefined) {
|
|
115700
|
+
this.seenApprovals.delete(oldest);
|
|
115701
|
+
}
|
|
115702
|
+
}
|
|
115703
|
+
}
|
|
115704
|
+
countUnconfirmed(approvalId) {
|
|
115705
|
+
this.unackedApprovals.delete(approvalId);
|
|
115706
|
+
this.unackedApprovals.set(approvalId, Date.now());
|
|
115707
|
+
this.notifyUnconfirmed();
|
|
115708
|
+
while (this.unackedApprovals.size > MAX_UNCONFIRMED_APPROVALS) {
|
|
115709
|
+
const oldest = this.unackedApprovals.keys().next().value;
|
|
115710
|
+
if (oldest === undefined) {
|
|
115711
|
+
break;
|
|
115712
|
+
}
|
|
115713
|
+
this.unackedApprovals.delete(oldest);
|
|
115714
|
+
}
|
|
115715
|
+
}
|
|
115716
|
+
settleApproval(approvalId, decision, applied, apply) {
|
|
115717
|
+
this.rememberSeen(approvalId, applied ? "applied" : "not-applied");
|
|
115718
|
+
try {
|
|
115719
|
+
this.options.onApprovalFrame?.({ approvalId, decision, applied });
|
|
115720
|
+
} catch {}
|
|
115721
|
+
apply?.();
|
|
115722
|
+
this.ackApproval(approvalId, applied);
|
|
115723
|
+
}
|
|
115724
|
+
parkEarlyDecision(approvalId, decision) {
|
|
115725
|
+
while (this.earlyDecisions.size >= MAX_EARLY_DECISIONS) {
|
|
115726
|
+
const oldest = this.earlyDecisions.keys().next().value;
|
|
115727
|
+
if (oldest === undefined) {
|
|
115728
|
+
break;
|
|
115729
|
+
}
|
|
115730
|
+
this.dropEarlyDecision(oldest);
|
|
115731
|
+
}
|
|
115732
|
+
const timer = setTimeout(() => this.dropEarlyDecision(approvalId), this.earlyDecisionMs);
|
|
115733
|
+
timer.unref?.();
|
|
115734
|
+
this.earlyDecisions.set(approvalId, { decision, timer });
|
|
115735
|
+
}
|
|
115736
|
+
dropEarlyDecision(approvalId) {
|
|
115737
|
+
const early = this.earlyDecisions.get(approvalId);
|
|
115738
|
+
if (early === undefined) {
|
|
115739
|
+
return;
|
|
115740
|
+
}
|
|
115741
|
+
clearTimeout(early.timer);
|
|
115742
|
+
this.earlyDecisions.delete(approvalId);
|
|
115743
|
+
if (!this.stopped) {
|
|
115744
|
+
this.settleApproval(approvalId, early.decision, false);
|
|
115745
|
+
}
|
|
115746
|
+
}
|
|
115747
|
+
async ackApproval(approvalId, applied) {
|
|
115748
|
+
let confirmed = false;
|
|
115749
|
+
try {
|
|
115750
|
+
const response = await this.post("approval-ack", { sessionId: this.options.sessionId, approvalId, applied });
|
|
115751
|
+
confirmed = response.ok;
|
|
115752
|
+
} catch {}
|
|
115753
|
+
if (this.stopped) {
|
|
115754
|
+
return;
|
|
115755
|
+
}
|
|
115756
|
+
if (confirmed) {
|
|
115757
|
+
this.unackedApprovals.delete(approvalId);
|
|
115758
|
+
this.notifyUnconfirmed();
|
|
115759
|
+
return;
|
|
115760
|
+
}
|
|
115761
|
+
this.lingerTimers.add(this.scheduleUnref(() => {
|
|
115762
|
+
this.notifyUnconfirmed();
|
|
115763
|
+
}, UNCONFIRMED_APPROVAL_LINGER_MS + 50));
|
|
115764
|
+
}
|
|
115765
|
+
notifyUnconfirmed() {
|
|
115766
|
+
try {
|
|
115767
|
+
this.options.onUnconfirmedChange?.();
|
|
115768
|
+
} catch {}
|
|
115769
|
+
}
|
|
115770
|
+
scheduleUnref(fn, ms) {
|
|
115771
|
+
const timer = setTimeout(() => {
|
|
115772
|
+
this.lingerTimers.delete(timer);
|
|
115773
|
+
fn();
|
|
115774
|
+
}, ms);
|
|
115775
|
+
timer.unref?.();
|
|
115776
|
+
return timer;
|
|
115777
|
+
}
|
|
115648
115778
|
resolveApproval(approvalId, decision) {
|
|
115649
115779
|
const waiter = this.waiters.get(approvalId);
|
|
115650
115780
|
if (waiter === undefined) {
|
|
115651
|
-
if (this.earlyDecisions.size >= MAX_EARLY_DECISIONS) {
|
|
115652
|
-
const oldest = this.earlyDecisions.keys().next().value;
|
|
115653
|
-
if (oldest !== undefined) {
|
|
115654
|
-
this.earlyDecisions.delete(oldest);
|
|
115655
|
-
}
|
|
115656
|
-
}
|
|
115657
|
-
this.earlyDecisions.set(approvalId, decision);
|
|
115658
115781
|
return;
|
|
115659
115782
|
}
|
|
115660
115783
|
clearTimeout(waiter.timer);
|
|
@@ -115691,11 +115814,13 @@ class RemoteClient {
|
|
|
115691
115814
|
this.waiters.delete(id);
|
|
115692
115815
|
waiter.resolve("deny");
|
|
115693
115816
|
}
|
|
115694
|
-
this.earlyDecisions.
|
|
115817
|
+
for (const id of [...this.earlyDecisions.keys()]) {
|
|
115818
|
+
this.dropEarlyDecision(id);
|
|
115819
|
+
}
|
|
115695
115820
|
this.failChoices();
|
|
115696
115821
|
}
|
|
115697
115822
|
}
|
|
115698
|
-
var UNVERIFIED_SERVE_MESSAGE = "the program answering on keryx serve's port did not prove it is the keryx serve this shell trusts; its answer was ignored. Restart `keryx serve` (an older serve cannot prove itself; update keryx on both sides)", FatalClientError, TransientClientError, DEFAULT_BACKOFF_INITIAL_MS = 250, DEFAULT_BACKOFF_MAX_MS = 15000, DEFAULT_REQUEST_TIMEOUT_MS = 1e4, MAX_EARLY_DECISIONS = 32, MAX_COMPLETED_IDS = 256;
|
|
115823
|
+
var UNVERIFIED_SERVE_MESSAGE = "the program answering on keryx serve's port did not prove it is the keryx serve this shell trusts; its answer was ignored. Restart `keryx serve` (an older serve cannot prove itself; update keryx on both sides)", FatalClientError, TransientClientError, DEFAULT_BACKOFF_INITIAL_MS = 250, DEFAULT_BACKOFF_MAX_MS = 15000, DEFAULT_REQUEST_TIMEOUT_MS = 1e4, MAX_EARLY_DECISIONS = 32, DEFAULT_EARLY_DECISION_MS = 2000, MAX_SEEN_APPROVALS = 256, UNCONFIRMED_APPROVAL_LINGER_MS = 60000, MAX_UNCONFIRMED_APPROVALS = 64, MAX_COMPLETED_IDS = 256;
|
|
115699
115824
|
var init_client3 = __esm(() => {
|
|
115700
115825
|
init_serve_config();
|
|
115701
115826
|
init_lease();
|
|
@@ -116375,10 +116500,12 @@ class RemoteBridge {
|
|
|
116375
116500
|
}
|
|
116376
116501
|
const online = this.clientConnected && client.connected;
|
|
116377
116502
|
const heartbeat = client.lastHeartbeatAt;
|
|
116503
|
+
const unconfirmed = client.unconfirmedApprovals ?? 0;
|
|
116378
116504
|
return {
|
|
116379
116505
|
state: online ? "on" : "offline",
|
|
116380
116506
|
...client.name !== undefined ? { name: client.name } : {},
|
|
116381
116507
|
...heartbeat !== undefined ? { heartbeatAgeMs: Math.max(0, this.now() - heartbeat) } : {},
|
|
116508
|
+
...unconfirmed > 0 ? { unconfirmedApprovals: unconfirmed } : {},
|
|
116382
116509
|
events: [...this.events]
|
|
116383
116510
|
};
|
|
116384
116511
|
}
|
|
@@ -116405,7 +116532,9 @@ class RemoteBridge {
|
|
|
116405
116532
|
...name !== undefined ? { name } : {},
|
|
116406
116533
|
...this.options.dir !== undefined ? { dir: this.options.dir } : {},
|
|
116407
116534
|
onLine: (text, meta) => this.accept(text, meta),
|
|
116408
|
-
onStatus: (status) => this.onStatus(status)
|
|
116535
|
+
onStatus: (status) => this.onStatus(status),
|
|
116536
|
+
onApprovalFrame: (frame) => this.host.notice(frame.applied ? `\u25C7 approval ${frame.approvalId} ${frame.decision === "allow" ? "allowed" : "denied"} from Telegram` : `\u25C7 approval ${frame.approvalId}: ${frame.decision === "allow" ? "an allow" : "a deny"} from Telegram arrived but nothing here was waiting for it; not applied`),
|
|
116537
|
+
onUnconfirmedChange: () => this.host.onChange?.()
|
|
116409
116538
|
};
|
|
116410
116539
|
const client = this.options.makeClient !== undefined ? this.options.makeClient(clientOptions) : new RemoteClient(clientOptions);
|
|
116411
116540
|
this.client = client;
|
|
@@ -118034,6 +118163,10 @@ function formatRemoteStatusLines(status) {
|
|
|
118034
118163
|
lines.push(`Last heartbeat: ${status.heartbeatAgeMs === undefined ? "none yet" : `${formatAge(status.heartbeatAgeMs)} ago`}`);
|
|
118035
118164
|
if (status.state === "offline")
|
|
118036
118165
|
lines.push("Serve is not reachable. The shell keeps retrying; nothing is lost from Telegram.");
|
|
118166
|
+
if (status.unconfirmedApprovals !== undefined && status.unconfirmedApprovals > 0) {
|
|
118167
|
+
const n = status.unconfirmedApprovals;
|
|
118168
|
+
lines.push(`Approvals not confirmed: ${n}. The topic shows ${n === 1 ? "it" : "them"} as "not confirmed" until serve takes the shell's ack.`);
|
|
118169
|
+
}
|
|
118037
118170
|
lines.push(`Turn it off with ${REMOTE_CONTROL_COMMAND} off, or press o here. Rename: ${REMOTE_CONTROL_COMMAND} off, then ${REMOTE_CONTROL_COMMAND} <name>.`);
|
|
118038
118171
|
return lines;
|
|
118039
118172
|
}
|
|
@@ -118154,7 +118287,11 @@ function projectRemoteRow(status, width) {
|
|
|
118154
118287
|
return { text: fit3("off", width), role: "muted", action: "open" };
|
|
118155
118288
|
if (status.state === "offline")
|
|
118156
118289
|
return { text: fit3("offline", width), role: "attention", action: "open" };
|
|
118157
|
-
|
|
118290
|
+
const topic = status.name ?? "connecting";
|
|
118291
|
+
const unconfirmed = status.unconfirmedApprovals ?? 0;
|
|
118292
|
+
if (unconfirmed > 0)
|
|
118293
|
+
return { text: fit3(`${topic} \xB7 ${unconfirmed} unconfirmed`, width), role: "attention", action: "open" };
|
|
118294
|
+
return { text: fit3(topic, width), role: "ok", action: "open" };
|
|
118158
118295
|
}
|
|
118159
118296
|
function mountRemotePanel(otui, renderer, parent, options) {
|
|
118160
118297
|
const core = otui;
|
|
@@ -121375,7 +121512,7 @@ var init_serve_turn_store = __esm(() => {
|
|
|
121375
121512
|
import { randomUUID as randomUUID34 } from "crypto";
|
|
121376
121513
|
import { existsSync as existsSync45, readdirSync as readdirSync10 } from "fs";
|
|
121377
121514
|
import path261 from "path";
|
|
121378
|
-
function
|
|
121515
|
+
function isApprovalId2(value) {
|
|
121379
121516
|
return ID_PATTERN2.test(value);
|
|
121380
121517
|
}
|
|
121381
121518
|
function root(dir) {
|
|
@@ -121401,7 +121538,7 @@ function appendEvidence(event, dir) {
|
|
|
121401
121538
|
appendOwnerOnlyLine(ledgerFile(dir), JSON.stringify(event));
|
|
121402
121539
|
}
|
|
121403
121540
|
function createApproval(input, dir, now = new Date) {
|
|
121404
|
-
if (!
|
|
121541
|
+
if (!isApprovalId2(input.approvalId)) {
|
|
121405
121542
|
throw new Error("approvalId is not an approval id");
|
|
121406
121543
|
}
|
|
121407
121544
|
ensureKeryxSubdir(["approvals"], dir);
|
|
@@ -121468,7 +121605,7 @@ function parseResolution(text) {
|
|
|
121468
121605
|
}
|
|
121469
121606
|
}
|
|
121470
121607
|
function readRaw(id, dir) {
|
|
121471
|
-
if (!
|
|
121608
|
+
if (!isApprovalId2(id)) {
|
|
121472
121609
|
return { ok: false, reason: "not-an-approval-id" };
|
|
121473
121610
|
}
|
|
121474
121611
|
const read = readConfigFile(requestFile(id, dir));
|
|
@@ -121572,7 +121709,7 @@ function approvalIds(dir) {
|
|
|
121572
121709
|
if (!existsSync45(base)) {
|
|
121573
121710
|
return [];
|
|
121574
121711
|
}
|
|
121575
|
-
return readdirSync10(base).filter((name) => name.endsWith(".json") && !name.endsWith(".resolution.json")).map((name) => name.slice(0, -".json".length)).filter(
|
|
121712
|
+
return readdirSync10(base).filter((name) => name.endsWith(".json") && !name.endsWith(".resolution.json")).map((name) => name.slice(0, -".json".length)).filter(isApprovalId2);
|
|
121576
121713
|
}
|
|
121577
121714
|
function listApprovals(dir, options = {}) {
|
|
121578
121715
|
const views = [];
|
|
@@ -141950,6 +142087,98 @@ function drainIdleMainQueue(items, options) {
|
|
|
141950
142087
|
return true;
|
|
141951
142088
|
}
|
|
141952
142089
|
|
|
142090
|
+
// src/tui/remote-queue-wiring.ts
|
|
142091
|
+
function createRemoteQueueWiring(deps) {
|
|
142092
|
+
let pendingEdit;
|
|
142093
|
+
return {
|
|
142094
|
+
removeMainQueue(index) {
|
|
142095
|
+
const queue = deps.getQueue();
|
|
142096
|
+
if (index < 0 || index >= queue.length)
|
|
142097
|
+
return;
|
|
142098
|
+
const removed = queue[index];
|
|
142099
|
+
if (removed?.source === TG_SOURCE) {
|
|
142100
|
+
deps.getBridge()?.queuedLineRemoved(removed.question);
|
|
142101
|
+
}
|
|
142102
|
+
deps.setQueue(removeMainQueueItem(queue, index));
|
|
142103
|
+
deps.paint();
|
|
142104
|
+
},
|
|
142105
|
+
editMainQueue(index) {
|
|
142106
|
+
const edited = editMainQueueItem(deps.getQueue(), index);
|
|
142107
|
+
if (edited === undefined)
|
|
142108
|
+
return;
|
|
142109
|
+
deps.setQueue(edited.rest);
|
|
142110
|
+
pendingEdit = pendingQueueEditFor(edited.removed, index);
|
|
142111
|
+
deps.composer.setText(edited.text);
|
|
142112
|
+
deps.composer.focus();
|
|
142113
|
+
deps.paint();
|
|
142114
|
+
},
|
|
142115
|
+
requeuePendingEdit(line, displayLine) {
|
|
142116
|
+
if (pendingEdit === undefined)
|
|
142117
|
+
return false;
|
|
142118
|
+
const edit = pendingEdit;
|
|
142119
|
+
pendingEdit = undefined;
|
|
142120
|
+
deps.setQueue(reinsertEditedMainQueueItem(deps.getQueue(), edit, line, displayLine));
|
|
142121
|
+
deps.paint();
|
|
142122
|
+
return true;
|
|
142123
|
+
},
|
|
142124
|
+
hasPendingEdit: () => pendingEdit !== undefined,
|
|
142125
|
+
stopRemoteForSessionSwitch() {
|
|
142126
|
+
const bridge = deps.getBridge();
|
|
142127
|
+
if (bridge?.keepingTopic === true) {
|
|
142128
|
+
bridge.sessionLeaving();
|
|
142129
|
+
return;
|
|
142130
|
+
}
|
|
142131
|
+
if (bridge?.active !== true)
|
|
142132
|
+
return;
|
|
142133
|
+
const closing = bridge.disable();
|
|
142134
|
+
deps.say(`\u25C7 remote control is turning off: the session changed, and its Telegram topic is being deleted. Turn it on again with /remote-control <name>.
|
|
142135
|
+
`);
|
|
142136
|
+
closing.then(() => deps.say(`\u25C7 remote control off: the topic is deleted.
|
|
142137
|
+
`));
|
|
142138
|
+
},
|
|
142139
|
+
dropQueuedTelegramLines() {
|
|
142140
|
+
const { kept, dropped } = dropQueuedBySource(deps.getQueue(), TG_SOURCE);
|
|
142141
|
+
if (dropped.length === 0)
|
|
142142
|
+
return [];
|
|
142143
|
+
deps.setQueue(kept);
|
|
142144
|
+
deps.paint();
|
|
142145
|
+
return dropped.map((item) => item.question);
|
|
142146
|
+
},
|
|
142147
|
+
enqueueTelegramLine(text) {
|
|
142148
|
+
const queue = deps.getQueue();
|
|
142149
|
+
queue.push({ id: deps.nextId(), question: text, displayQuestion: deps.summarize(text), source: TG_SOURCE });
|
|
142150
|
+
deps.paint();
|
|
142151
|
+
deps.say(`\u25C7 a line from Telegram is queued as q${queue.length}.
|
|
142152
|
+
`);
|
|
142153
|
+
},
|
|
142154
|
+
runTelegramLine(text) {
|
|
142155
|
+
deps.runLine(text, "operator", TG_SOURCE);
|
|
142156
|
+
},
|
|
142157
|
+
runQueued(item) {
|
|
142158
|
+
deps.runLine(item.question, "operator", item.source);
|
|
142159
|
+
},
|
|
142160
|
+
turnStarted(source) {
|
|
142161
|
+
deps.getBridge()?.turnStarted(source);
|
|
142162
|
+
},
|
|
142163
|
+
assistantText(text) {
|
|
142164
|
+
deps.getBridge()?.assistantText(text);
|
|
142165
|
+
},
|
|
142166
|
+
wrapOnToolCall(base) {
|
|
142167
|
+
return (...args) => {
|
|
142168
|
+
deps.getBridge()?.toolCall();
|
|
142169
|
+
base?.(...args);
|
|
142170
|
+
};
|
|
142171
|
+
},
|
|
142172
|
+
turnSettled(outcome) {
|
|
142173
|
+
const bridge = deps.getBridge();
|
|
142174
|
+
return bridge === undefined ? Promise.resolve() : bridge.turnSettled({ failed: outcome.failed || outcome.aborted });
|
|
142175
|
+
}
|
|
142176
|
+
};
|
|
142177
|
+
}
|
|
142178
|
+
var init_remote_queue_wiring = __esm(() => {
|
|
142179
|
+
init_shell_bridge();
|
|
142180
|
+
});
|
|
142181
|
+
|
|
141953
142182
|
// src/tui/worker-fleet.ts
|
|
141954
142183
|
function clip4(s, max) {
|
|
141955
142184
|
if (s.length <= max) {
|
|
@@ -151763,17 +151992,31 @@ async function launchTuiAgentShell(opts) {
|
|
|
151763
151992
|
guardCollector.onToolResult(name, toolResult);
|
|
151764
151993
|
baseOnToolResultForGuard?.(name, toolResult);
|
|
151765
151994
|
};
|
|
151995
|
+
const remoteQueue = createRemoteQueueWiring({
|
|
151996
|
+
getBridge: () => remoteBridge,
|
|
151997
|
+
getQueue: () => mainQueue,
|
|
151998
|
+
setQueue: (next) => {
|
|
151999
|
+
mainQueue = next;
|
|
152000
|
+
},
|
|
152001
|
+
nextId: () => `mq${mainQueueSeq++}`,
|
|
152002
|
+
summarize: summarizeSubmittedLine,
|
|
152003
|
+
paint: () => paintMainQueue(),
|
|
152004
|
+
composer: {
|
|
152005
|
+
setText: (text) => {
|
|
152006
|
+
input.value = text;
|
|
152007
|
+
},
|
|
152008
|
+
focus: () => input.focus()
|
|
152009
|
+
},
|
|
152010
|
+
say: (text) => io.onSystem?.(text),
|
|
152011
|
+
runLine: (line, origin, source) => runLine(line, origin, source)
|
|
152012
|
+
});
|
|
151766
152013
|
const baseOnAssistantTextForGuard = io.onAssistantText?.bind(io);
|
|
151767
152014
|
io.onAssistantText = (text) => {
|
|
151768
152015
|
guardCollector.onAssistantText(text);
|
|
151769
|
-
|
|
152016
|
+
remoteQueue.assistantText(text);
|
|
151770
152017
|
baseOnAssistantTextForGuard?.(text);
|
|
151771
152018
|
};
|
|
151772
|
-
|
|
151773
|
-
io.onToolCall = (name, toolInput) => {
|
|
151774
|
-
remoteBridge?.toolCall();
|
|
151775
|
-
baseOnToolCallForRemote?.(name, toolInput);
|
|
151776
|
-
};
|
|
152019
|
+
io.onToolCall = remoteQueue.wrapOnToolCall(io.onToolCall?.bind(io));
|
|
151777
152020
|
const refreshGuardSidebar = () => {
|
|
151778
152021
|
clearTranscriptChildren(sbGuard);
|
|
151779
152022
|
if (!guardEnabled)
|
|
@@ -152419,19 +152662,7 @@ async function launchTuiAgentShell(opts) {
|
|
|
152419
152662
|
deps = { ...deps, onContextCompaction };
|
|
152420
152663
|
liveDeps = deps;
|
|
152421
152664
|
let splash = undefined;
|
|
152422
|
-
const stopRemoteForSessionSwitch = () =>
|
|
152423
|
-
if (remoteBridge?.keepingTopic === true) {
|
|
152424
|
-
remoteBridge.sessionLeaving();
|
|
152425
|
-
return;
|
|
152426
|
-
}
|
|
152427
|
-
if (remoteBridge?.active !== true)
|
|
152428
|
-
return;
|
|
152429
|
-
const closing = remoteBridge.disable();
|
|
152430
|
-
io.onSystem?.(`\u25C7 remote control is turning off: the session changed, and its Telegram topic is being deleted. Turn it on again with /remote-control <name>.
|
|
152431
|
-
`);
|
|
152432
|
-
closing.then(() => io.onSystem?.(`\u25C7 remote control off: the topic is deleted.
|
|
152433
|
-
`));
|
|
152434
|
-
};
|
|
152665
|
+
const stopRemoteForSessionSwitch = () => remoteQueue.stopRemoteForSessionSwitch();
|
|
152435
152666
|
const applyOpened = (opened, previewHistory) => {
|
|
152436
152667
|
if (opened.history.length > 0) {
|
|
152437
152668
|
splash?.removeIfShown();
|
|
@@ -153567,7 +153798,6 @@ ${formatThemeList(getThemeId())}`);
|
|
|
153567
153798
|
const sideQueue = [];
|
|
153568
153799
|
let mainQueue = [];
|
|
153569
153800
|
let mainQueueSeq = 0;
|
|
153570
|
-
let pendingQueueEdit;
|
|
153571
153801
|
const forceHandoff = createForegroundForceHandoff();
|
|
153572
153802
|
let sideWorkerRunning = false;
|
|
153573
153803
|
let sideClearTimeout;
|
|
@@ -153675,26 +153905,8 @@ ${formatThemeList(getThemeId())}`);
|
|
|
153675
153905
|
fleet.remove("agent:queue");
|
|
153676
153906
|
}
|
|
153677
153907
|
};
|
|
153678
|
-
const removeMainQueue = (index) =>
|
|
153679
|
-
|
|
153680
|
-
return;
|
|
153681
|
-
const removedItem = mainQueue[index];
|
|
153682
|
-
if (removedItem?.source === TG_SOURCE) {
|
|
153683
|
-
remoteBridge?.queuedLineRemoved(removedItem.question);
|
|
153684
|
-
}
|
|
153685
|
-
mainQueue = removeMainQueueItem(mainQueue, index);
|
|
153686
|
-
paintMainQueue();
|
|
153687
|
-
};
|
|
153688
|
-
const editMainQueue = (index) => {
|
|
153689
|
-
const edited = editMainQueueItem(mainQueue, index);
|
|
153690
|
-
if (edited === undefined)
|
|
153691
|
-
return;
|
|
153692
|
-
mainQueue = edited.rest;
|
|
153693
|
-
pendingQueueEdit = pendingQueueEditFor(edited.removed, index);
|
|
153694
|
-
input.value = edited.text;
|
|
153695
|
-
input.focus();
|
|
153696
|
-
paintMainQueue();
|
|
153697
|
-
};
|
|
153908
|
+
const removeMainQueue = (index) => remoteQueue.removeMainQueue(index);
|
|
153909
|
+
const editMainQueue = (index) => remoteQueue.editMainQueue(index);
|
|
153698
153910
|
const forceMainQueue = (index) => {
|
|
153699
153911
|
const item = mainQueue[index];
|
|
153700
153912
|
if (item === undefined)
|
|
@@ -153709,7 +153921,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
153709
153921
|
forceForegroundQueueItem(foregroundOperation, forceHandoff, item, {
|
|
153710
153922
|
announce: () => io.onSystem?.(`\u25C7 main turn interrupted \u2014 q${index + 1} will run next.
|
|
153711
153923
|
`),
|
|
153712
|
-
run: (next) =>
|
|
153924
|
+
run: (next) => remoteQueue.runQueued(next)
|
|
153713
153925
|
});
|
|
153714
153926
|
};
|
|
153715
153927
|
const handleQueueNavKey = (key) => {
|
|
@@ -154118,11 +154330,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
154118
154330
|
case "not-a-command":
|
|
154119
154331
|
break;
|
|
154120
154332
|
}
|
|
154121
|
-
if (
|
|
154122
|
-
const edit = pendingQueueEdit;
|
|
154123
|
-
pendingQueueEdit = undefined;
|
|
154124
|
-
mainQueue = reinsertEditedMainQueueItem(mainQueue, edit, line, displayLine);
|
|
154125
|
-
paintMainQueue();
|
|
154333
|
+
if (remoteQueue.requeuePendingEdit(line, displayLine)) {
|
|
154126
154334
|
return;
|
|
154127
154335
|
}
|
|
154128
154336
|
if (leaseView()?.held() === true) {
|
|
@@ -154163,7 +154371,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
154163
154371
|
takeForced: () => forceHandoff.takeNext(),
|
|
154164
154372
|
dispatch: (next) => {
|
|
154165
154373
|
paintMainQueue();
|
|
154166
|
-
|
|
154374
|
+
remoteQueue.runQueued(next);
|
|
154167
154375
|
}
|
|
154168
154376
|
});
|
|
154169
154377
|
} else {
|
|
@@ -154939,7 +155147,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
154939
155147
|
const next = forceHandoff.takeNext() ?? mainQueue.shift();
|
|
154940
155148
|
paintMainQueue();
|
|
154941
155149
|
if (next !== undefined)
|
|
154942
|
-
|
|
155150
|
+
remoteQueue.runQueued(next);
|
|
154943
155151
|
}
|
|
154944
155152
|
}
|
|
154945
155153
|
});
|
|
@@ -154951,7 +155159,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
154951
155159
|
sessions.clear();
|
|
154952
155160
|
deps.resetSubagentBudget?.();
|
|
154953
155161
|
const operation = foregroundOperation.begin();
|
|
154954
|
-
|
|
155162
|
+
remoteQueue.turnStarted(source);
|
|
154955
155163
|
blockIo.resetReasoningLiveState();
|
|
154956
155164
|
setMainAgent("running", "waiting");
|
|
154957
155165
|
startBusy("waiting for model");
|
|
@@ -155046,7 +155254,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
155046
155254
|
...slateSession !== undefined ? { slateSession, pruneArchive: true } : {}
|
|
155047
155255
|
}).finally(() => {
|
|
155048
155256
|
foregroundOperation.settle(operation);
|
|
155049
|
-
|
|
155257
|
+
remoteQueue.turnSettled({ failed: turnFailed, aborted: turnSignal.aborted });
|
|
155050
155258
|
if (foregroundOperation.isDisposed)
|
|
155051
155259
|
return;
|
|
155052
155260
|
refreshRewindSidebar();
|
|
@@ -155099,7 +155307,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
155099
155307
|
const next = forceHandoff.takeNext() ?? mainQueue.shift();
|
|
155100
155308
|
paintMainQueue();
|
|
155101
155309
|
if (next !== undefined) {
|
|
155102
|
-
|
|
155310
|
+
remoteQueue.runQueued(next);
|
|
155103
155311
|
} else {
|
|
155104
155312
|
busWakeController?.onSettle();
|
|
155105
155313
|
}
|
|
@@ -155143,7 +155351,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
155143
155351
|
const drained = forceHandoff.takeNext() ?? mainQueue.shift();
|
|
155144
155352
|
paintMainQueue();
|
|
155145
155353
|
if (drained !== undefined)
|
|
155146
|
-
|
|
155354
|
+
remoteQueue.runQueued(drained);
|
|
155147
155355
|
}
|
|
155148
155356
|
});
|
|
155149
155357
|
const captureShellOutput = async (run) => {
|
|
@@ -155255,14 +155463,8 @@ Turn: ${chrome.isBusy() ? "running" : "idle"} \xB7 queue: ${mainQueue.length} wa
|
|
|
155255
155463
|
sessionId: () => liveSession.summary.id,
|
|
155256
155464
|
project: () => sessionCwd,
|
|
155257
155465
|
isBusy: () => chrome.isBusy() || foregroundOperation.isActive || forceHandoff.isAwaitingSettlement || mainQueue.length > 0 || leaseView()?.held() === true,
|
|
155258
|
-
runLine: (text) =>
|
|
155259
|
-
enqueue: (text) =>
|
|
155260
|
-
const id = `mq${mainQueueSeq++}`;
|
|
155261
|
-
mainQueue.push({ id, question: text, displayQuestion: summarizeSubmittedLine(text), source: TG_SOURCE });
|
|
155262
|
-
paintMainQueue();
|
|
155263
|
-
io.onSystem?.(`\u25C7 a line from Telegram is queued as q${mainQueue.length}.
|
|
155264
|
-
`);
|
|
155265
|
-
},
|
|
155466
|
+
runLine: (text) => remoteQueue.runTelegramLine(text),
|
|
155467
|
+
enqueue: (text) => remoteQueue.enqueueTelegramLine(text),
|
|
155266
155468
|
notice: (text) => io.onSystem?.(`${text}
|
|
155267
155469
|
`),
|
|
155268
155470
|
cancelTurn: () => foregroundOperation.cancel("remote run time limit"),
|
|
@@ -155280,14 +155482,7 @@ Turn: ${chrome.isBusy() ? "running" : "idle"} \xB7 queue: ${mainQueue.length} wa
|
|
|
155280
155482
|
liveSession = recordRemoteOff(liveSession);
|
|
155281
155483
|
} catch {}
|
|
155282
155484
|
},
|
|
155283
|
-
dropQueuedTelegramLines: () =>
|
|
155284
|
-
const { kept, dropped } = dropQueuedBySource(mainQueue, TG_SOURCE);
|
|
155285
|
-
if (dropped.length === 0)
|
|
155286
|
-
return [];
|
|
155287
|
-
mainQueue = kept;
|
|
155288
|
-
paintMainQueue();
|
|
155289
|
-
return dropped.map((item) => item.question);
|
|
155290
|
-
},
|
|
155485
|
+
dropQueuedTelegramLines: () => remoteQueue.dropQueuedTelegramLines(),
|
|
155291
155486
|
runCommand: runRemoteCommand,
|
|
155292
155487
|
busyRefusal: (line) => {
|
|
155293
155488
|
const command = findAgentCommand(line, "agent");
|
|
@@ -155514,6 +155709,7 @@ var init_tui_shell = __esm(() => {
|
|
|
155514
155709
|
init_composer_choice();
|
|
155515
155710
|
init_filter_list();
|
|
155516
155711
|
init_shell_chrome();
|
|
155712
|
+
init_remote_queue_wiring();
|
|
155517
155713
|
init_queue_nav();
|
|
155518
155714
|
init_subagent_inspector();
|
|
155519
155715
|
init_subagent_session();
|
|
@@ -192911,7 +193107,7 @@ function listApprovalsRoute(request, ctx) {
|
|
|
192911
193107
|
});
|
|
192912
193108
|
}
|
|
192913
193109
|
async function answerApprovalRoute(request, approvalId, ctx) {
|
|
192914
|
-
if (!
|
|
193110
|
+
if (!isApprovalId2(approvalId)) {
|
|
192915
193111
|
return approvalNotFound();
|
|
192916
193112
|
}
|
|
192917
193113
|
const declared = Number(request.headers.get("content-length") ?? "0");
|
|
@@ -193178,6 +193374,7 @@ var init_serve_server = __esm(() => {
|
|
|
193178
193374
|
["prompt", "POST"],
|
|
193179
193375
|
["state", "POST"],
|
|
193180
193376
|
["ack", "POST"],
|
|
193377
|
+
["approval-ack", "POST"],
|
|
193181
193378
|
["stream", "GET"],
|
|
193182
193379
|
["channels-status", "GET"],
|
|
193183
193380
|
["channels-pair", "POST"],
|
|
@@ -196665,6 +196862,7 @@ class RemoteHttpSurface {
|
|
|
196665
196862
|
timers;
|
|
196666
196863
|
keepaliveMs;
|
|
196667
196864
|
ackTimeoutMs;
|
|
196865
|
+
approvalAckMs;
|
|
196668
196866
|
newApprovalId;
|
|
196669
196867
|
newPromptId;
|
|
196670
196868
|
streams = new Map;
|
|
@@ -196673,6 +196871,8 @@ class RemoteHttpSurface {
|
|
|
196673
196871
|
bound = new Map;
|
|
196674
196872
|
pendingAcks = new Map;
|
|
196675
196873
|
approvals = new Map;
|
|
196874
|
+
awaiting = new Map;
|
|
196875
|
+
decided = new Map;
|
|
196676
196876
|
choices = new Map;
|
|
196677
196877
|
finished = new Map;
|
|
196678
196878
|
constructor(options) {
|
|
@@ -196681,6 +196881,7 @@ class RemoteHttpSurface {
|
|
|
196681
196881
|
this.timers = options.timers ?? realTimers;
|
|
196682
196882
|
this.keepaliveMs = options.keepaliveMs ?? DEFAULT_KEEPALIVE_MS;
|
|
196683
196883
|
this.ackTimeoutMs = options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS;
|
|
196884
|
+
this.approvalAckMs = options.approvalAckMs ?? DEFAULT_APPROVAL_ACK_MS;
|
|
196684
196885
|
this.newApprovalId = options.approvalIds ?? (() => `ap${randomBytes15(6).toString("hex")}`);
|
|
196685
196886
|
this.newPromptId = options.promptIds ?? (() => `pk${randomBytes15(6).toString("hex")}`);
|
|
196686
196887
|
this.consumer = {
|
|
@@ -196747,6 +196948,8 @@ class RemoteHttpSurface {
|
|
|
196747
196948
|
return this.messageState(hub, request);
|
|
196748
196949
|
case "ack":
|
|
196749
196950
|
return this.ack(hub, request);
|
|
196951
|
+
case "approval-ack":
|
|
196952
|
+
return this.approvalAck(hub, request);
|
|
196750
196953
|
case "stream":
|
|
196751
196954
|
return this.openStream(hub, request, io);
|
|
196752
196955
|
}
|
|
@@ -196765,6 +196968,9 @@ class RemoteHttpSurface {
|
|
|
196765
196968
|
for (const id of [...this.approvals.keys()]) {
|
|
196766
196969
|
this.dropApproval(id);
|
|
196767
196970
|
}
|
|
196971
|
+
for (const id of [...this.awaiting.keys()]) {
|
|
196972
|
+
this.dropAwaiting(id);
|
|
196973
|
+
}
|
|
196768
196974
|
for (const id of [...this.choices.keys()]) {
|
|
196769
196975
|
this.dropChoice(id);
|
|
196770
196976
|
}
|
|
@@ -197088,6 +197294,48 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
197088
197294
|
this.complete(value.sessionId, value.updateId);
|
|
197089
197295
|
return ok2({ acknowledged: true });
|
|
197090
197296
|
}
|
|
197297
|
+
async approvalAck(hub, request) {
|
|
197298
|
+
const body = await readJsonBody(request);
|
|
197299
|
+
if (!body.ok) {
|
|
197300
|
+
return body.response;
|
|
197301
|
+
}
|
|
197302
|
+
const value = body.value;
|
|
197303
|
+
if (!isSessionId(value.sessionId)) {
|
|
197304
|
+
return invalid2("sessionId must be 1 to 64 characters of letters, digits, '-' and '_'.");
|
|
197305
|
+
}
|
|
197306
|
+
if (!isApprovalId(value.approvalId)) {
|
|
197307
|
+
return invalid2("approvalId must be 'ap' and 12 lowercase hex characters.");
|
|
197308
|
+
}
|
|
197309
|
+
if (!hub.hasSession(value.sessionId)) {
|
|
197310
|
+
return fail4(404, "unknown-session", "This session is not registered for remote control; register again.");
|
|
197311
|
+
}
|
|
197312
|
+
if (value.applied !== undefined && typeof value.applied !== "boolean") {
|
|
197313
|
+
return invalid2("applied must be true or false when present.");
|
|
197314
|
+
}
|
|
197315
|
+
const sessionId = value.sessionId;
|
|
197316
|
+
const id = value.approvalId;
|
|
197317
|
+
const waiting = this.awaiting.get(id);
|
|
197318
|
+
if (waiting !== undefined && waiting.sessionId === sessionId) {
|
|
197319
|
+
this.dropAwaiting(id);
|
|
197320
|
+
this.rememberDecided(id, sessionId);
|
|
197321
|
+
if (value.applied === false) {
|
|
197322
|
+
this.finishApproval(id, sessionId, waiting.messageId, waiting.text, {
|
|
197323
|
+
result: `Not applied at ${waiting.sentAt}: the shell was no longer waiting for this question, so the ${waiting.decision === "allow" ? "Allow" : "Deny"} from ${waiting.who} changed nothing.`,
|
|
197324
|
+
shortReply: "Not applied: the shell was no longer waiting for this question."
|
|
197325
|
+
});
|
|
197326
|
+
return ok2({ acknowledged: true });
|
|
197327
|
+
}
|
|
197328
|
+
this.finishApproval(id, sessionId, waiting.messageId, waiting.text, {
|
|
197329
|
+
result: waiting.decision === "allow" ? `Allowed by ${waiting.who} at ${waiting.sentAt}.` : `Denied by ${waiting.who} at ${waiting.sentAt}.`,
|
|
197330
|
+
shortReply: waiting.decision === "allow" ? "Approval granted." : "Approval denied."
|
|
197331
|
+
});
|
|
197332
|
+
return ok2({ acknowledged: true });
|
|
197333
|
+
}
|
|
197334
|
+
if (this.decided.get(id) === sessionId) {
|
|
197335
|
+
return ok2({ acknowledged: true });
|
|
197336
|
+
}
|
|
197337
|
+
return fail4(404, "unknown-approval", "No approval with that id was sent to this session.");
|
|
197338
|
+
}
|
|
197091
197339
|
openStream(hub, request, io) {
|
|
197092
197340
|
const sessionId = new URL(request.url).searchParams.get("sessionId");
|
|
197093
197341
|
if (!isSessionId(sessionId)) {
|
|
@@ -197244,10 +197492,14 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
197244
197492
|
});
|
|
197245
197493
|
return;
|
|
197246
197494
|
}
|
|
197247
|
-
|
|
197248
|
-
|
|
197249
|
-
|
|
197250
|
-
|
|
197495
|
+
this.awaiting.set(id, {
|
|
197496
|
+
sessionId,
|
|
197497
|
+
decision: approval.decision,
|
|
197498
|
+
who: `user ${callback.fromId}`,
|
|
197499
|
+
sentAt: when,
|
|
197500
|
+
...messageId === undefined ? {} : { messageId },
|
|
197501
|
+
...entry.text === undefined ? {} : { text: entry.text },
|
|
197502
|
+
timer: this.timers.setTimeout(() => this.giveUpOnAck(id), this.approvalAckMs)
|
|
197251
197503
|
});
|
|
197252
197504
|
return;
|
|
197253
197505
|
}
|
|
@@ -197255,6 +197507,9 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
197255
197507
|
this.expireApproval(id);
|
|
197256
197508
|
return;
|
|
197257
197509
|
}
|
|
197510
|
+
if (this.awaiting.has(id)) {
|
|
197511
|
+
return;
|
|
197512
|
+
}
|
|
197258
197513
|
const done = this.finished.get(id);
|
|
197259
197514
|
if (done !== undefined) {
|
|
197260
197515
|
if (done.sessionId === sessionId) {
|
|
@@ -197275,7 +197530,7 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
197275
197530
|
}
|
|
197276
197531
|
}
|
|
197277
197532
|
carriesLivePrompt(messageId) {
|
|
197278
|
-
return [...this.approvals.values(), ...this.choices.values()].some((live) => live.messageId === messageId);
|
|
197533
|
+
return [...this.approvals.values(), ...this.awaiting.values(), ...this.choices.values()].some((live) => live.messageId === messageId);
|
|
197279
197534
|
}
|
|
197280
197535
|
pressChoice(sessionId, callback, choice, stream) {
|
|
197281
197536
|
const id = choice.promptId;
|
|
@@ -197358,7 +197613,7 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
197358
197613
|
});
|
|
197359
197614
|
}
|
|
197360
197615
|
onApprovalSent(approvalId, sessionId, messageId, text) {
|
|
197361
|
-
const live = this.approvals.get(approvalId);
|
|
197616
|
+
const live = this.approvals.get(approvalId) ?? this.awaiting.get(approvalId);
|
|
197362
197617
|
if (live !== undefined) {
|
|
197363
197618
|
live.messageId = messageId;
|
|
197364
197619
|
live.text = text;
|
|
@@ -197411,6 +197666,37 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
197411
197666
|
this.timers.clearTimeout(entry.timer);
|
|
197412
197667
|
this.approvals.delete(approvalId);
|
|
197413
197668
|
}
|
|
197669
|
+
dropAwaiting(approvalId) {
|
|
197670
|
+
const entry = this.awaiting.get(approvalId);
|
|
197671
|
+
if (entry === undefined) {
|
|
197672
|
+
return;
|
|
197673
|
+
}
|
|
197674
|
+
this.timers.clearTimeout(entry.timer);
|
|
197675
|
+
this.awaiting.delete(approvalId);
|
|
197676
|
+
}
|
|
197677
|
+
rememberDecided(approvalId, sessionId) {
|
|
197678
|
+
this.decided.delete(approvalId);
|
|
197679
|
+
this.decided.set(approvalId, sessionId);
|
|
197680
|
+
while (this.decided.size > MAX_DECIDED_APPROVALS) {
|
|
197681
|
+
const oldest = this.decided.keys().next().value;
|
|
197682
|
+
if (oldest === undefined) {
|
|
197683
|
+
break;
|
|
197684
|
+
}
|
|
197685
|
+
this.decided.delete(oldest);
|
|
197686
|
+
}
|
|
197687
|
+
}
|
|
197688
|
+
giveUpOnAck(approvalId) {
|
|
197689
|
+
const entry = this.awaiting.get(approvalId);
|
|
197690
|
+
if (entry === undefined) {
|
|
197691
|
+
return;
|
|
197692
|
+
}
|
|
197693
|
+
this.dropAwaiting(approvalId);
|
|
197694
|
+
this.rememberDecided(approvalId, entry.sessionId);
|
|
197695
|
+
this.finishApproval(approvalId, entry.sessionId, entry.messageId, entry.text, {
|
|
197696
|
+
result: `Sent to the shell at ${entry.sentAt}, not confirmed; the shell denies by itself if it did not receive it.`,
|
|
197697
|
+
shortReply: "Sent to the shell, not confirmed; it denies by itself if it did not receive it."
|
|
197698
|
+
});
|
|
197699
|
+
}
|
|
197414
197700
|
expireApproval(approvalId) {
|
|
197415
197701
|
const entry = this.approvals.get(approvalId);
|
|
197416
197702
|
if (entry === undefined) {
|
|
@@ -197447,6 +197733,11 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
197447
197733
|
}
|
|
197448
197734
|
}
|
|
197449
197735
|
}
|
|
197736
|
+
for (const [id, entry] of [...this.awaiting.entries()]) {
|
|
197737
|
+
if (entry.sessionId === stream.sessionId && !this.closed) {
|
|
197738
|
+
this.giveUpOnAck(id);
|
|
197739
|
+
}
|
|
197740
|
+
}
|
|
197450
197741
|
for (const [id, entry] of [...this.choices.entries()]) {
|
|
197451
197742
|
if (entry.sessionId === stream.sessionId) {
|
|
197452
197743
|
this.dropChoice(id);
|
|
@@ -197472,7 +197763,7 @@ function asCodeBlock(text) {
|
|
|
197472
197763
|
${safe}
|
|
197473
197764
|
${fence}`;
|
|
197474
197765
|
}
|
|
197475
|
-
var MAX_REPLY_CHARS = 20000, MAX_PENDING_APPROVALS_PER_SESSION = 8, MAX_PENDING_CHOICES_PER_SESSION = 8, MAX_REMEMBERED_IDS_PER_SESSION = 512, MAX_PROJECT_NAME_CHARS = 128, CONTROL_CHARS2, JSON_HEADERS2, MAX_FINISHED_PROMPTS = 128, MAX_SETTLED_TEXT_CHARS = 3600, MAX_BACKTICK_RUN = 8;
|
|
197766
|
+
var MAX_REPLY_CHARS = 20000, MAX_PENDING_APPROVALS_PER_SESSION = 8, MAX_PENDING_CHOICES_PER_SESSION = 8, MAX_REMEMBERED_IDS_PER_SESSION = 512, MAX_PROJECT_NAME_CHARS = 128, CONTROL_CHARS2, JSON_HEADERS2, MAX_FINISHED_PROMPTS = 128, MAX_DECIDED_APPROVALS = 256, MAX_SETTLED_TEXT_CHARS = 3600, MAX_BACKTICK_RUN = 8;
|
|
197476
197767
|
var init_http_surface = __esm(() => {
|
|
197477
197768
|
init_service2();
|
|
197478
197769
|
init_hub();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.67",
|
|
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": {
|