@tiens.nguyen/gonext-local-worker 1.0.234 → 1.0.236
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-local-worker.mjs +7 -1
- package/gonext-repl.mjs +62 -3
- package/gonext_agent_chat.py +33 -10
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -2043,7 +2043,13 @@ async function runAgentChatJob(job) {
|
|
|
2043
2043
|
method: "PATCH",
|
|
2044
2044
|
body: JSON.stringify({
|
|
2045
2045
|
jobStatus: "completed",
|
|
2046
|
-
|
|
2046
|
+
// When the answer streamed live (answer_stream), completed resultText must stay
|
|
2047
|
+
// an EXACT EXTENSION of the chunk accumulation: the REPL renders the final poll
|
|
2048
|
+
// by flushing resultText.slice(<chars already shown>), so replacing the stream
|
|
2049
|
+
// with the shorter bare answer here made short plain replies render cut
|
|
2050
|
+
// mid-word ("Hello! How"). Only when nothing streamed is the deduped finalText
|
|
2051
|
+
// the whole story.
|
|
2052
|
+
resultText: answerStreamed ? fullText : finalText || fullText,
|
|
2047
2053
|
tokenCount: Math.max(1, Math.ceil((finalText || fullText).length / 4)),
|
|
2048
2054
|
totalTimeSeconds,
|
|
2049
2055
|
}),
|
package/gonext-repl.mjs
CHANGED
|
@@ -994,6 +994,14 @@ async function runAgentTurn(history) {
|
|
|
994
994
|
// (see carryFlushedLen below); the agent path's code/tool-call lines must stay buffered
|
|
995
995
|
// until a full line is known, or we'd risk showing part of something we meant to hide.
|
|
996
996
|
let plainReplyFlow = false;
|
|
997
|
+
// Exact answer characters printed live in the plain-reply flow — lets the completed
|
|
998
|
+
// poll figure out how much of the FINAL answer is still unshown even when resultText
|
|
999
|
+
// was replaced (not extended) on completion. See the completed-branch recovery below.
|
|
1000
|
+
let liveAnswer = "";
|
|
1001
|
+
// First few KB of the raw stream as consumed — a cheap fingerprint to verify the
|
|
1002
|
+
// completed resultText really EXTENDS what we streamed (an old worker replaces it
|
|
1003
|
+
// with the shorter bare answer, which must not be sliced by shownChars).
|
|
1004
|
+
let seenRaw = "";
|
|
997
1005
|
|
|
998
1006
|
// --- Live in-progress ("blinking") bullet -------------------------------------------
|
|
999
1007
|
// The agent's raw Thought/code stream is SUPPRESSED (see the ~~~-fence handling in
|
|
@@ -1235,6 +1243,7 @@ async function runAgentTurn(history) {
|
|
|
1235
1243
|
// Plain-reply content IS the answer — print it in the default color and remember
|
|
1236
1244
|
// it's visible so the caller doesn't reprint it once the turn completes.
|
|
1237
1245
|
process.stdout.write(line.slice(alreadyFlushed) + "\n");
|
|
1246
|
+
liveAnswer += line.slice(alreadyFlushed) + "\n";
|
|
1238
1247
|
answerShownLive = true;
|
|
1239
1248
|
} else {
|
|
1240
1249
|
// The remaining out-of-fence ACTION events ("Command passed → …", "Searching code
|
|
@@ -1251,6 +1260,7 @@ async function runAgentTurn(history) {
|
|
|
1251
1260
|
if (plainReplyFlow && !inCode && carry.length > carryFlushedLen) {
|
|
1252
1261
|
if (carryFlushedLen === 0) onContent();
|
|
1253
1262
|
process.stdout.write(carry.slice(carryFlushedLen)); // default color — this IS the answer
|
|
1263
|
+
liveAnswer += carry.slice(carryFlushedLen);
|
|
1254
1264
|
carryFlushedLen = carry.length;
|
|
1255
1265
|
answerShownLive = true;
|
|
1256
1266
|
lastWasBlank = false;
|
|
@@ -1323,14 +1333,38 @@ async function runAgentTurn(history) {
|
|
|
1323
1333
|
// caller (shown=false), so a live flush there is unnecessary (and would compound
|
|
1324
1334
|
// the separate agent double-print). This runs for the completed poll only; the
|
|
1325
1335
|
// running-branch consume below handles every earlier poll.
|
|
1326
|
-
|
|
1336
|
+
//
|
|
1337
|
+
// The suffix math is only valid when the completed resultText EXTENDS the
|
|
1338
|
+
// streamed accumulation. An older worker REPLACES it with the bare deduped
|
|
1339
|
+
// answer — a shorter, different string — so slicing by shownChars either shows
|
|
1340
|
+
// nothing (the reported "Hello! How" cut) or a garbage tail. Verify with the
|
|
1341
|
+
// seenRaw fingerprint; on a mismatch, recover by answer-prefix instead: print
|
|
1342
|
+
// whatever of the final answer wasn't already shown live.
|
|
1343
|
+
const extendsStream = text.length >= shownChars && text.startsWith(seenRaw);
|
|
1344
|
+
if (plainReplyFlow && extendsStream && text.length > shownChars) {
|
|
1327
1345
|
consume(text.slice(shownChars));
|
|
1328
1346
|
shownChars = text.length;
|
|
1347
|
+
} else if (plainReplyFlow && !extendsStream && answerShownLive) {
|
|
1348
|
+
const ans = answerFrom(text);
|
|
1349
|
+
let rest = ans.startsWith(liveAnswer) ? ans.slice(liveAnswer.length) : null;
|
|
1350
|
+
if (rest === null) {
|
|
1351
|
+
// The live text may carry a trailing newline the trimmed answer lacks.
|
|
1352
|
+
const printed = liveAnswer.replace(/\s+$/, "");
|
|
1353
|
+
rest = ans.startsWith(printed) ? ans.slice(printed.length) : null;
|
|
1354
|
+
}
|
|
1355
|
+
if (rest) {
|
|
1356
|
+
onContent();
|
|
1357
|
+
process.stdout.write(rest);
|
|
1358
|
+
lastWasBlank = false;
|
|
1359
|
+
}
|
|
1329
1360
|
}
|
|
1330
1361
|
clearStatus();
|
|
1331
1362
|
return { text: answerFrom(text), shown: answerShownLive };
|
|
1332
1363
|
}
|
|
1333
1364
|
if (text.length > shownChars) {
|
|
1365
|
+
if (seenRaw.length < 4096) {
|
|
1366
|
+
seenRaw = (seenRaw + text.slice(shownChars)).slice(0, 4096);
|
|
1367
|
+
}
|
|
1334
1368
|
consume(text.slice(shownChars));
|
|
1335
1369
|
shownChars = text.length;
|
|
1336
1370
|
}
|
|
@@ -1470,6 +1504,11 @@ async function main() {
|
|
|
1470
1504
|
process.on("SIGINT", onInterrupt);
|
|
1471
1505
|
rl.on("SIGINT", onInterrupt);
|
|
1472
1506
|
|
|
1507
|
+
// The task from the most recent Ctrl+C-cancelled turn — so a bare "continue" right after
|
|
1508
|
+
// resumes it. A cancelled turn is popped from history (nothing to continue otherwise), so
|
|
1509
|
+
// without this "continue" reaches the agent with no task and it replies as generic chat.
|
|
1510
|
+
let lastCancelledTask = "";
|
|
1511
|
+
|
|
1473
1512
|
for (;;) {
|
|
1474
1513
|
rl.prompt();
|
|
1475
1514
|
// Absorb blank Enter-presses SILENTLY under the same prompt — without this, lines
|
|
@@ -1536,7 +1575,22 @@ async function main() {
|
|
|
1536
1575
|
console.log(dim(`unknown command: ${cmd} — type /help (or / then Tab) to list commands\n`));
|
|
1537
1576
|
continue;
|
|
1538
1577
|
}
|
|
1539
|
-
|
|
1578
|
+
// Resume a cancelled task: a BARE "continue" right after a Ctrl+C re-runs the ORIGINAL
|
|
1579
|
+
// request. Only the immediately-next input may resume (consumed here) and only a bare
|
|
1580
|
+
// continue-word — a message with real instructions is a new task. Slash commands above
|
|
1581
|
+
// used `continue` and never reach here, so `/model` then "continue" still resumes.
|
|
1582
|
+
const pendingResume = lastCancelledTask;
|
|
1583
|
+
lastCancelledTask = "";
|
|
1584
|
+
let taskLine = line;
|
|
1585
|
+
if (
|
|
1586
|
+
pendingResume &&
|
|
1587
|
+
/^(continue|resume|go on|keep going|carry on|proceed)\.?$/i.test(line)
|
|
1588
|
+
) {
|
|
1589
|
+
taskLine = pendingResume;
|
|
1590
|
+
const shown = pendingResume.length > 80 ? pendingResume.slice(0, 80) + "…" : pendingResume;
|
|
1591
|
+
console.log(dim(`↻ resuming the cancelled task: ${shown}\n`));
|
|
1592
|
+
}
|
|
1593
|
+
history.push({ role: "user", content: taskLine });
|
|
1540
1594
|
try {
|
|
1541
1595
|
const { text: answer, shown, cancelled } = await runAgentTurn(history);
|
|
1542
1596
|
if (answer) {
|
|
@@ -1552,7 +1606,12 @@ async function main() {
|
|
|
1552
1606
|
history.pop(); // aborted/failed/cancelled turn — don't poison the next request
|
|
1553
1607
|
// Ctrl+C cancel already printed its own "(cancelled)" note in runAgentTurn — a
|
|
1554
1608
|
// second "(no answer)" line here would be redundant/misleading.
|
|
1555
|
-
if (
|
|
1609
|
+
if (cancelled) {
|
|
1610
|
+
// Remember the ACTUAL task that ran (taskLine, not a bare "continue") so the
|
|
1611
|
+
// next bare "continue" resumes it — even if it was cancelled during step 1,
|
|
1612
|
+
// before any worker-side checkpoint step was written.
|
|
1613
|
+
lastCancelledTask = taskLine;
|
|
1614
|
+
} else {
|
|
1556
1615
|
console.log(red("\n(no answer — run aborted or failed)\n"));
|
|
1557
1616
|
}
|
|
1558
1617
|
}
|
package/gonext_agent_chat.py
CHANGED
|
@@ -1926,20 +1926,31 @@ _WS_ACTIVE: str = ""
|
|
|
1926
1926
|
|
|
1927
1927
|
|
|
1928
1928
|
def _rag_read_allowed(path: str) -> str:
|
|
1929
|
-
"""Resolve `path` and ensure it stays within a safe root (
|
|
1930
|
-
|
|
1931
|
-
arbitrary files like ~/.ssh. Relative paths anchor to the terminal workspace
|
|
1932
|
-
downloads/unzips land),
|
|
1933
|
-
|
|
1929
|
+
"""Resolve `path` and ensure it stays within a safe root (temp dir, ~/.gonext, the
|
|
1930
|
+
active terminal folder, or a registered workspace) — so the agent's file tools can't
|
|
1931
|
+
read arbitrary files like ~/.ssh. Relative paths anchor to the terminal workspace
|
|
1932
|
+
(where downloads/unzips land), because the model routinely passes bare names like
|
|
1933
|
+
"tools-hook" or "tools-hook/README.md".
|
|
1934
|
+
|
|
1935
|
+
NOTE: the worker's PROCESS CWD is deliberately NOT an allowed root (bug #70). The
|
|
1936
|
+
daemon is often launched from ~/Projects (the PARENT of every workspace), so trusting
|
|
1937
|
+
os.getcwd() let `grep_repo(path="/Users/joseph/Projects")` read EVERY project on disk
|
|
1938
|
+
— including other repos' secrets. Reads are confined to the registered/active
|
|
1939
|
+
workspaces + ~/.gonext + the temp dir. Fail-closed: no workspace ⇒ no repo reads."""
|
|
1934
1940
|
import os
|
|
1935
1941
|
import tempfile
|
|
1936
1942
|
expanded = os.path.expanduser((path or "").strip())
|
|
1937
1943
|
if not os.path.isabs(expanded):
|
|
1938
|
-
|
|
1944
|
+
# Anchor a relative path to the active workspace (NOT the worker cwd — see above).
|
|
1945
|
+
base = _WS_ACTIVE or (_WS_ROOTS[0]["path"] if _WS_ROOTS else None)
|
|
1946
|
+
if base is None:
|
|
1947
|
+
raise ValueError(
|
|
1948
|
+
"No workspace is active — register a folder with `gonext-local-worker "
|
|
1949
|
+
"workspace add <path>` before reading files."
|
|
1950
|
+
)
|
|
1939
1951
|
expanded = os.path.join(base, expanded)
|
|
1940
1952
|
rp = os.path.realpath(expanded)
|
|
1941
1953
|
roots = [
|
|
1942
|
-
os.path.realpath(os.getcwd()),
|
|
1943
1954
|
os.path.realpath(tempfile.gettempdir()),
|
|
1944
1955
|
os.path.realpath(os.path.join(os.path.expanduser("~"), ".gonext")),
|
|
1945
1956
|
] + [w["path"] for w in _WS_ROOTS]
|
|
@@ -1950,8 +1961,8 @@ def _rag_read_allowed(path: str) -> str:
|
|
|
1950
1961
|
if any(rp == r or rp.startswith(r + os.sep) for r in roots):
|
|
1951
1962
|
return rp
|
|
1952
1963
|
raise ValueError(
|
|
1953
|
-
"Path is outside
|
|
1954
|
-
"
|
|
1964
|
+
f"Path '{path}' is outside your registered workspace(s). The agent can only read "
|
|
1965
|
+
"inside a folder you registered (or download/unzip outputs), not the whole disk."
|
|
1955
1966
|
)
|
|
1956
1967
|
|
|
1957
1968
|
|
|
@@ -3040,6 +3051,9 @@ def run_agent_chat(cfg):
|
|
|
3040
3051
|
f" CURRENT folder — the terminal is open HERE; default ALL create/edit/run/"
|
|
3041
3052
|
f"download operations to THIS folder unless the user EXPLICITLY names another "
|
|
3042
3053
|
f"workspace: {_active_label}\n"
|
|
3054
|
+
" Ignore any OTHER folder paths that appear only in EARLIER messages (a previous "
|
|
3055
|
+
"task, a cancelled turn) — they are stale. Do NOT grep/list/read across parent "
|
|
3056
|
+
"directories or unrelated projects; work in the CURRENT folder above.\n"
|
|
3043
3057
|
if _active_label else ""
|
|
3044
3058
|
)
|
|
3045
3059
|
# Upfront, ZERO-COST (no tool call, no model round-trip) top-level listing — so the
|
|
@@ -5196,8 +5210,17 @@ def run_agent_chat(cfg):
|
|
|
5196
5210
|
# even during a total coding-model outage, instead of trusting a plain-chat
|
|
5197
5211
|
# model's guess that "there's no code or project" (seen live — false: the
|
|
5198
5212
|
# workspace had real content the whole time, the model just never reached it).
|
|
5213
|
+
# ONLY the current/active folder — never dump the full list of every registered
|
|
5214
|
+
# workspace here. That list (t1..t23) used to get persisted into the answer/history
|
|
5215
|
+
# and then confused the next turn into free-roaming other projects (bug #70).
|
|
5199
5216
|
if _WS_AVAILABLE:
|
|
5200
|
-
|
|
5217
|
+
import os as _os
|
|
5218
|
+
_ov_path = _WS_ACTIVE or (_WS_ROOTS[0]["path"] if _WS_ROOTS else "")
|
|
5219
|
+
_ov = next((w for w in _WS_ROOTS if w["path"] == _ov_path), None)
|
|
5220
|
+
if _ov is None and _ov_path:
|
|
5221
|
+
_ov = {"name": _os.path.basename(_ov_path), "path": _ov_path}
|
|
5222
|
+
if _ov:
|
|
5223
|
+
note += f"\n\n(Current folder {_ov['name']}:\n{_workspace_overview([_ov])})"
|
|
5201
5224
|
_emit({"type": "final", "text": note})
|
|
5202
5225
|
|
|
5203
5226
|
|
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.236",
|
|
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",
|