@tiens.nguyen/gonext-local-worker 1.0.309 → 1.0.310
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/gonext-repl.mjs +71 -1
- package/gonext_agent_chat.py +13 -3
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -1113,6 +1113,63 @@ function pickFromList(items, initialSel = 0, opts = {}) {
|
|
|
1113
1113
|
});
|
|
1114
1114
|
}
|
|
1115
1115
|
|
|
1116
|
+
// Cancel whichever approval picker is currently showing (Yes/No or the N-row list),
|
|
1117
|
+
// resolving it as a DENY. Used when the job ends server-side while we're still waiting
|
|
1118
|
+
// for the user, so the picker promise never leaks and no late choice is sent (#110).
|
|
1119
|
+
function cancelActivePicker() {
|
|
1120
|
+
if (approvalActive) finishApproval(false);
|
|
1121
|
+
if (listPickerActive) finishList(-1);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Show an approval picker, but RACE it against the job going terminal. The old code
|
|
1125
|
+
// awaited the picker INLINE inside the poll loop, so while the user thought, the REPL
|
|
1126
|
+
// stopped polling and couldn't notice that python had already given up (its own approval
|
|
1127
|
+
// deadline) and completed the job — the user's late "Yes" then hit a finished job and was
|
|
1128
|
+
// silently lost (#110). Here a background watcher polls the job; if it goes
|
|
1129
|
+
// completed/failed/cancelled first, we dismiss the picker and report `ended:true` so the
|
|
1130
|
+
// caller skips the POST and lets the normal terminal-status handling run.
|
|
1131
|
+
async function approvalPromptRacingJob(command) {
|
|
1132
|
+
let watching = true;
|
|
1133
|
+
const watcher = (async () => {
|
|
1134
|
+
while (watching) {
|
|
1135
|
+
await sleep(1000);
|
|
1136
|
+
if (!watching) return null;
|
|
1137
|
+
try {
|
|
1138
|
+
const r = await fetch(`${apiBase}/api/worker/jobs/${jobIdForApprovalRace}`, {
|
|
1139
|
+
headers: { "X-Worker-Key": workerKey },
|
|
1140
|
+
});
|
|
1141
|
+
if (!r.ok) continue;
|
|
1142
|
+
const j = await r.json().catch(() => ({}));
|
|
1143
|
+
if (
|
|
1144
|
+
j?.jobStatus === "completed" ||
|
|
1145
|
+
j?.jobStatus === "failed" ||
|
|
1146
|
+
j?.jobStatus === "cancelled"
|
|
1147
|
+
) {
|
|
1148
|
+
return "ended";
|
|
1149
|
+
}
|
|
1150
|
+
} catch {
|
|
1151
|
+
/* transient poll error — keep watching */
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return null;
|
|
1155
|
+
})();
|
|
1156
|
+
const picker = approvalPrompt(command).then((r) => ({ kind: "answer", ...r }));
|
|
1157
|
+
const raced = await Promise.race([
|
|
1158
|
+
picker,
|
|
1159
|
+
watcher.then((v) => (v === "ended" ? { kind: "ended" } : new Promise(() => {}))),
|
|
1160
|
+
]);
|
|
1161
|
+
watching = false;
|
|
1162
|
+
if (raced.kind === "ended") {
|
|
1163
|
+
cancelActivePicker(); // unblock the picker promise so it doesn't leak
|
|
1164
|
+
await picker.catch(() => {}); // let it settle after the cancel
|
|
1165
|
+
return { ended: true, allow: false, dontAsk: false };
|
|
1166
|
+
}
|
|
1167
|
+
// The user answered first — stop the watcher and return their choice.
|
|
1168
|
+
return { ended: false, allow: raced.allow, dontAsk: raced.dontAsk };
|
|
1169
|
+
}
|
|
1170
|
+
// The jobId the approval race should watch — set by runAgentTurn before it can prompt.
|
|
1171
|
+
let jobIdForApprovalRace = "";
|
|
1172
|
+
|
|
1116
1173
|
// Per-session coding-model override chosen via /model (empty = use the account default).
|
|
1117
1174
|
// Sent to /agent-ask as codingModelOverride; the API validates it against the whitelist.
|
|
1118
1175
|
let sessionCodingModel = "";
|
|
@@ -1326,6 +1383,7 @@ async function runAgentTurn(history) {
|
|
|
1326
1383
|
following = true;
|
|
1327
1384
|
followAborted = false;
|
|
1328
1385
|
currentJobId = jobId;
|
|
1386
|
+
jobIdForApprovalRace = jobId; // so an approval picker can watch THIS job for early end (#110)
|
|
1329
1387
|
cancelRequested = false;
|
|
1330
1388
|
const startedAt = Date.now();
|
|
1331
1389
|
let shownChars = 0;
|
|
@@ -1894,7 +1952,19 @@ async function runAgentTurn(history) {
|
|
|
1894
1952
|
) {
|
|
1895
1953
|
lastApprovalId = job.pendingApproval.id;
|
|
1896
1954
|
clearStatus();
|
|
1897
|
-
|
|
1955
|
+
// Race the picker against the job ending server-side (#110): if python gives up
|
|
1956
|
+
// (its own approval deadline) and completes the job while we're waiting, dismiss
|
|
1957
|
+
// the picker and DON'T post a now-useless choice — fall through to the normal
|
|
1958
|
+
// completed/failed/cancelled handling below on the next poll.
|
|
1959
|
+
const { ended, allow, dontAsk } = await approvalPromptRacingJob(
|
|
1960
|
+
job.pendingApproval.command || ""
|
|
1961
|
+
);
|
|
1962
|
+
if (ended) {
|
|
1963
|
+
process.stdout.write(
|
|
1964
|
+
dim("\n (the run already ended before you chose — reply 'continue' to resume)\n")
|
|
1965
|
+
);
|
|
1966
|
+
continue; // re-poll; the terminal-status branches below take it from here
|
|
1967
|
+
}
|
|
1898
1968
|
// "Yes, don't ask again this session" (max-step prompt only): remember it so every
|
|
1899
1969
|
// later turn sends alwaysExtendOnMaxStep and the agent auto-extends silently (#80).
|
|
1900
1970
|
if (dontAsk) {
|
package/gonext_agent_chat.py
CHANGED
|
@@ -2835,10 +2835,16 @@ def _ws_command_risk(command, argv, denyset):
|
|
|
2835
2835
|
return (None, None)
|
|
2836
2836
|
|
|
2837
2837
|
|
|
2838
|
-
def _ws_request_approval(api_base, worker_key, job_id, command, reason):
|
|
2838
|
+
def _ws_request_approval(api_base, worker_key, job_id, command, reason, deadline_s=180):
|
|
2839
2839
|
"""Pause and ask the user (via the terminal REPL's Yes/No picker) to allow a risky
|
|
2840
2840
|
command. Registers a pending approval on the job through the API, then polls the job
|
|
2841
2841
|
for the user's decision. Returns True (allow) or False (deny / timeout / cancelled).
|
|
2842
|
+
|
|
2843
|
+
deadline_s bounds the wait. 180s is right for a RISKY command (absent user → deny,
|
|
2844
|
+
never auto-run). But the __MAXSTEP__ "keep going?" prompt is NOT security-sensitive and
|
|
2845
|
+
the coder can be slow, so it passes a much larger deadline (#110): the REPL now
|
|
2846
|
+
dismisses the picker the instant this job ends, and the worker's own job-cap kills a
|
|
2847
|
+
truly-abandoned run, so a long wait here is safe and lets a real "Yes" actually land.
|
|
2842
2848
|
Mirrors _pdf_upload_via_api's worker-key auth — no new creds on the worker."""
|
|
2843
2849
|
import urllib.request as _u
|
|
2844
2850
|
import uuid as _uuid
|
|
@@ -2866,7 +2872,7 @@ def _ws_request_approval(api_base, worker_key, job_id, command, reason):
|
|
|
2866
2872
|
_emit({"type": "step", "text": "Awaiting your choice: compact the context?"})
|
|
2867
2873
|
else:
|
|
2868
2874
|
_emit({"type": "step", "text": f"Awaiting your approval to run: {command[:70]}"})
|
|
2869
|
-
deadline = _t.time() +
|
|
2875
|
+
deadline = _t.time() + max(30, int(deadline_s)) # per-call; risky=180s, __MAXSTEP__ long
|
|
2870
2876
|
while _t.time() < deadline:
|
|
2871
2877
|
_t.sleep(1.2)
|
|
2872
2878
|
try:
|
|
@@ -6543,10 +6549,14 @@ def run_agent_chat(cfg):
|
|
|
6543
6549
|
"(don't-ask-again session flag)")
|
|
6544
6550
|
_do_extend = True
|
|
6545
6551
|
elif _interactive_approval and _job_id and pdf_api_base and pdf_worker_key:
|
|
6552
|
+
# Long deadline (#110): the "keep going?" prompt isn't risky, and the
|
|
6553
|
+
# slow coder + a thinking user shouldn't get auto-denied at 180s. The
|
|
6554
|
+
# REPL dismisses the picker if this job ends, and the worker job-cap
|
|
6555
|
+
# bounds a walked-away user, so waiting here is safe.
|
|
6546
6556
|
_do_extend = _ws_request_approval(
|
|
6547
6557
|
pdf_api_base, pdf_worker_key, _job_id,
|
|
6548
6558
|
f"__MAXSTEP__::Reached the step budget ({agent.max_steps}). "
|
|
6549
|
-
"Keep going?", "max-steps")
|
|
6559
|
+
"Keep going?", "max-steps", deadline_s=1500)
|
|
6550
6560
|
else:
|
|
6551
6561
|
_do_extend = False # web / no interactive channel → current wrap-up
|
|
6552
6562
|
if not _do_extend:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.310",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|