@tiens.nguyen/gonext-local-worker 1.0.252 → 1.0.254

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.
@@ -1984,6 +1984,8 @@ async function runAgentChatJob(job) {
1984
1984
  // each python "tokens" event, persisted to the job doc (Mongo) so the REPL can show
1985
1985
  // it live and it survives on the completed job.
1986
1986
  let latestCodeTokens = 0;
1987
+ // Task #84: peak single-request prompt-token count seen this turn (not the sum).
1988
+ let latestMaxCodeTokens = 0;
1987
1989
  // Raw model-stream tokens (event.type === "stream") are Python code + `Thought:`
1988
1990
  // prose + <code>/<|"|> fragments. The web Thought panel renders reasoning as
1989
1991
  // markdown, which turns `#`-prefixed code comments into <h1> headings and
@@ -2111,16 +2113,24 @@ async function runAgentChatJob(job) {
2111
2113
  enqueueText(event.text);
2112
2114
  answerStreamed = true;
2113
2115
  } else if (event.type === "tokens" && Number.isFinite(event.codeInput)) {
2114
- // Prompt tokens sent to the agent CODE model (task #83). Persist the running
2115
- // total to the job doc so it's in Mongo and the REPL can render it live. Steps
2116
- // are seconds apart, so a fire-and-forget PATCH per event is cheap; it only
2117
- // touches agentCodeTokens (never resultText/jobStatus), so it can't race the
2118
- // chunk/completed writes.
2119
- if (event.codeInput > latestCodeTokens) {
2120
- latestCodeTokens = event.codeInput;
2116
+ // Prompt tokens sent to the agent CODE model (tasks #83 accumulated + #84 peak).
2117
+ // Persist both to the job doc so they're in Mongo and the REPL can render them
2118
+ // live. Steps are seconds apart, so a fire-and-forget PATCH per event is cheap;
2119
+ // it only touches agentCodeTokens/agentMaxCodeTokens (never resultText/jobStatus),
2120
+ // so it can't race the chunk/completed writes.
2121
+ const nextTotal = Math.max(latestCodeTokens, event.codeInput);
2122
+ const nextMax = Number.isFinite(event.codeMax)
2123
+ ? Math.max(latestMaxCodeTokens, event.codeMax)
2124
+ : latestMaxCodeTokens;
2125
+ if (nextTotal !== latestCodeTokens || nextMax !== latestMaxCodeTokens) {
2126
+ latestCodeTokens = nextTotal;
2127
+ latestMaxCodeTokens = nextMax;
2121
2128
  workerFetch(`/api/worker/jobs/${jobId}`, {
2122
2129
  method: "PATCH",
2123
- body: JSON.stringify({ agentCodeTokens: latestCodeTokens }),
2130
+ body: JSON.stringify({
2131
+ agentCodeTokens: latestCodeTokens,
2132
+ agentMaxCodeTokens: latestMaxCodeTokens,
2133
+ }),
2124
2134
  }).catch(() => {});
2125
2135
  }
2126
2136
  } else if (event.type === "final" && typeof event.text === "string") {
@@ -2174,6 +2184,7 @@ async function runAgentChatJob(job) {
2174
2184
  resultText: answerStreamed ? fullText : finalText || fullText,
2175
2185
  tokenCount: Math.max(1, Math.ceil((finalText || fullText).length / 4)),
2176
2186
  ...(latestCodeTokens > 0 ? { agentCodeTokens: latestCodeTokens } : {}),
2187
+ ...(latestMaxCodeTokens > 0 ? { agentMaxCodeTokens: latestMaxCodeTokens } : {}),
2177
2188
  totalTimeSeconds,
2178
2189
  }),
2179
2190
  });
package/gonext-repl.mjs CHANGED
@@ -122,6 +122,7 @@ const SPINNERS = [
122
122
  // green, cyan, blue, indigo, pink, amber).
123
123
  const WAIT_COLORS_256 = [120, 87, 111, 147, 218, 221];
124
124
  const color256 = (n, s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
125
+ const orange = (s) => color256(208, s); // peak-token label (task #84)
125
126
  // Compact token count: 950 → "950", 12300 → "12.3k", 1500000 → "1.5M" (task #83).
126
127
  const fmtTokens = (n) => {
127
128
  if (n < 1000) return String(n);
@@ -564,6 +565,16 @@ async function loadSessionServer(cwd) {
564
565
  }
565
566
  }
566
567
 
568
+ // Peak single-request agent-code-model token count remembered per folder (task #84).
569
+ async function loadSessionMaxCodeTokens(cwd) {
570
+ try {
571
+ const raw = JSON.parse(await readFile(sessionFilePath(cwd), "utf8"));
572
+ return Number.isFinite(raw?.maxCodeTokens) ? raw.maxCodeTokens : 0;
573
+ } catch {
574
+ return 0;
575
+ }
576
+ }
577
+
567
578
  async function saveSession(cwd, history) {
568
579
  try {
569
580
  await mkdir(SESSIONS_DIR, { recursive: true });
@@ -576,6 +587,7 @@ async function saveSession(cwd, history) {
576
587
  updatedAt: new Date().toISOString(),
577
588
  testAuto: sessionTestAuto,
578
589
  deployServer: selectedServer, // remember the /server pick per folder (task #69)
590
+ maxCodeTokens: sessionMaxCodeTokens, // peak per-request tokens (task #84)
579
591
  history: trimmed,
580
592
  },
581
593
  null,
@@ -919,6 +931,10 @@ let alwaysExtendOnMaxStep = false;
919
931
  // Deploy target chosen via /server (task #69): {name, host, user} or null. Session-scoped;
920
932
  // sent to /agent-ask as deployServer so the agent deploys to this host with KEY auth.
921
933
  let selectedServer = null;
934
+ // Task #84: peak single-request agent-code-model token count for THIS workspace, kept
935
+ // as a max across turns and persisted per folder in the session file. Shown in orange
936
+ // next to the live token count.
937
+ let sessionMaxCodeTokens = 0;
922
938
 
923
939
  // While a turn runs, mute readline's own echo/redraws so keystrokes can't corrupt the
924
940
  // live status display and there's no way to type a new question — only Ctrl+C works.
@@ -1148,10 +1164,12 @@ async function runAgentTurn(history) {
1148
1164
  // Line 2 (indented): the SPINNER + the playful word together, both in the cycling
1149
1165
  // color — the spinner belongs with the word (dot on the label line, spinner leading
1150
1166
  // the flavor line).
1151
- // Task #83: after the playful word, ALWAYS show the running agent-code-model token
1152
- // count (dim), e.g. " ⠙ Caffeinating… · 12.3k tok" starts at "· 0 tok" and climbs
1153
- // as steps send more, so it's visible from the first thinking frame.
1154
- const tokLabel = dim(` · ${fmtTokens(latestCodeTokens)} tok`);
1167
+ // Task #83/#84: after the playful word, show the running agent-code-model token
1168
+ // count (dim) plus the per-workspace PEAK single-request count (orange), e.g.
1169
+ // " ⠙ Caffeinating… · 12.3k tok · 8.1k max". Both start at 0 and climb.
1170
+ const tokLabel =
1171
+ dim(` · ${fmtTokens(latestCodeTokens)} tok`) +
1172
+ orange(` · ${fmtTokens(sessionMaxCodeTokens)} max`);
1155
1173
  process.stdout.write(
1156
1174
  `${green(BULLET)} ${green(`${fit(primary)}… (${secs}s)`)}` +
1157
1175
  "\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`
@@ -1424,6 +1442,10 @@ async function runAgentTurn(history) {
1424
1442
  if (Number.isFinite(job.agentCodeTokens) && job.agentCodeTokens > latestCodeTokens) {
1425
1443
  latestCodeTokens = job.agentCodeTokens;
1426
1444
  }
1445
+ // Peak single-request tokens (task #84): keep the per-workspace max across turns.
1446
+ if (Number.isFinite(job.agentMaxCodeTokens) && job.agentMaxCodeTokens > sessionMaxCodeTokens) {
1447
+ sessionMaxCodeTokens = job.agentMaxCodeTokens;
1448
+ }
1427
1449
  const text = typeof job.resultText === "string" ? job.resultText : "";
1428
1450
  if (job.jobStatus === "completed") {
1429
1451
  // The job can flip to "completed" between polls with authoritative resultText that
@@ -1541,6 +1563,7 @@ async function main() {
1541
1563
  // reflect them (both survive a fresh terminal in the same project).
1542
1564
  sessionTestAuto = await loadSessionTestAuto(resolve(process.cwd()));
1543
1565
  selectedServer = await loadSessionServer(resolve(process.cwd()));
1566
+ sessionMaxCodeTokens = await loadSessionMaxCodeTokens(resolve(process.cwd()));
1544
1567
  // Show which models will answer, straight from user settings (also validates auth).
1545
1568
  try {
1546
1569
  const probe = await fetchAgentPayload([{ role: "user", content: "ping" }]);
@@ -3117,11 +3117,17 @@ def run_agent_chat(cfg):
3117
3117
  [_active_ws] if _active_ws else _WS_ROOTS
3118
3118
  ) if _WS_AVAILABLE else []
3119
3119
  _ws_overview = _workspace_overview(_overview_roots) if _overview_roots else ""
3120
- _ws_tool_block = (
3120
+ # VARIABLE workspace context (task #86): the active-folder line, workspace list and
3121
+ # the on-disk folder overview change between turns (the agent creates/edits files),
3122
+ # so they must NOT sit inside the static tool_hint — they'd bust the prefix cache
3123
+ # mid-reference every turn. They join the variable tail of task_with_hint instead.
3124
+ _ws_context_block = (
3121
3125
  _ws_current_line
3122
3126
  + f" Other registered workspaces (use only if the user names them): {_ws_names}\n"
3123
3127
  f" Current folder contents:\n"
3124
3128
  + "\n".join(f" {line}" for line in _ws_overview.splitlines()) + "\n"
3129
+ ) if _WS_AVAILABLE else ""
3130
+ _ws_tool_block = (
3125
3131
  " - list_dir(path) / read_text_file(path) — browse and read files.\n"
3126
3132
  " - grep_repo(pattern, path='', glob='') — search code, returns file:line hits. "
3127
3133
  "Use this to find the EXACT lines to change.\n"
@@ -3225,19 +3231,18 @@ def run_agent_chat(cfg):
3225
3231
  f"(Permission denied publickey,password), STOP and tell the user to run "
3226
3232
  f"'ssh-copy-id {_du}@{_dh}' once, then retry. ***\n"
3227
3233
  )
3234
+ # STATIC ONLY (task #86): everything in tool_hint must be byte-identical between
3235
+ # turns so the model server's prefix KV-cache covers it (see task_with_hint below).
3236
+ # Variable text — the deploy target and the current date/time — lives in the
3237
+ # variable tail next to the task instead.
3228
3238
  tool_hint = (
3229
- # Deploy target FIRST (when a server is picked) a small model buries context deep
3230
- # in a 20k-char hint, so the chosen server must lead, not trail (task #69 retest fix).
3231
- _deploy_block
3232
- + f"Solve the TASK step by step (up to {max_steps} steps). At EACH step write a "
3239
+ f"Solve the TASK step by step (up to {max_steps} steps). At EACH step write a "
3233
3240
  "brief Thought, then ONE code block that calls a SINGLE tool. You will SEE that "
3234
3241
  "tool's result (Observation) before the next step — use it to decide what to do "
3235
3242
  "next (e.g. web_search to find a URL, THEN fetch_url to read it). When you have "
3236
3243
  "enough to answer, call final_answer(<your answer>) in a code block. Prefer "
3237
3244
  "FEWER steps, and never repeat a tool call that already succeeded.\n\n"
3238
3245
  "{{TOOL_LIST}}\n" # filled in below, once from the REAL registered tool objects
3239
- f"(Current date/time: {now_str} — pass a timezone to get_current_datetime() only "
3240
- "if the task needs a DIFFERENT one.)\n"
3241
3246
  + _rag_tool_block + _ws_tool_block + _auto_test_block +
3242
3247
  "\n"
3243
3248
  "http_request RETURN FORMAT: 'HTTP 200\\n{body}' — first line is 'HTTP <code>', body follows.\n"
@@ -3322,14 +3327,30 @@ def run_agent_chat(cfg):
3322
3327
  f"Previous task: {_ck.get('task', '')}\n" + "\n".join(_kept)
3323
3328
  )
3324
3329
 
3325
- # Lead with the TASK so the weak model anchors on what's actually being asked —
3326
- # not on the tool reference below. (Previously the hint led with the date, and the
3327
- # 3B model treated every message as a date question.)
3330
+ # ORDER (task #86 prefix-cache fix): STATIC first, VARIABLE last.
3331
+ # The ~16k-char tool reference is byte-identical between turns; leading with it
3332
+ # (right after the equally static system prompt) lets the model server's prefix
3333
+ # KV-cache cover ~7k tokens across turns. Previously the variable task text came
3334
+ # FIRST, so every new turn forced a cold re-eval of the whole reference (~2 min to
3335
+ # first token on step 1 of each turn; steps 2+ were 10-30s because within a turn
3336
+ # the prompt only grows at the end).
3337
+ # The TASK now comes LAST, under a loud header: recency is the position a model
3338
+ # attends to most, so anchoring is preserved (the historical "hint led with the
3339
+ # date → 3B model answered everything as a date question" regression was about a
3340
+ # misleading LEAD, not about where the task sits). The two VARIABLE blocks — the
3341
+ # current date/time and the deploy target — live here in the tail, next to the
3342
+ # task: the deploy target must stay near the task, not buried mid-hint (#69), and
3343
+ # the timestamp changes every minute, which would bust the cache mid-reference.
3328
3344
  task_with_hint = (
3329
- "TASK (answer THIS, choose the tool that fits it):\n"
3330
- f"{task_text}\n\n"
3331
- "----- TOOL REFERENCE -----\n"
3332
- + tool_hint
3345
+ "----- TOOL REFERENCE (how to work your actual TASK follows below) -----\n"
3346
+ + tool_hint +
3347
+ "\n----- YOUR TASK -----\n"
3348
+ f"(Current date/time: {now_str} — pass a timezone to get_current_datetime() only "
3349
+ "if the task needs a DIFFERENT one.)\n"
3350
+ + _deploy_block
3351
+ + _ws_context_block +
3352
+ "\nTASK (answer THIS, choose the tool that fits it):\n"
3353
+ f"{task_text}\n"
3333
3354
  )
3334
3355
 
3335
3356
  # Track URLs that have already failed so we don't retry dead endpoints across steps.
@@ -3940,6 +3961,9 @@ def run_agent_chat(cfg):
3940
3961
  # turn (task #83). Only this wrapper counts — the chat/brain helper calls
3941
3962
  # (_chat_completion) don't route through here, so this is "code model only".
3942
3963
  self._code_tokens_total = 0
3964
+ # Largest SINGLE-request prompt-token count seen this turn (task #84) — the
3965
+ # peak context size, not the sum. The REPL keeps a per-workspace max of this.
3966
+ self._code_tokens_max = 0
3943
3967
  # stream_agent: request the completion with stream=True and reassemble the
3944
3968
  # deltas into one message. Enabled for Ollama, where a long single-shot
3945
3969
  # (non-stream) generation sends nothing until the very end — a reverse proxy
@@ -4182,6 +4206,19 @@ def run_agent_chat(cfg):
4182
4206
  # _mark_first), read by the heartbeat below to switch its wording.
4183
4207
  self._first_token_seen = False
4184
4208
 
4209
+ # Task #83: emit the PROMPT-token estimate NOW, at request time — so the
4210
+ # code-model token count climbs the instant a step is SENT, not only after a
4211
+ # slow step returns usage (a cold gemma4 can wait minutes for the first token,
4212
+ # during which the count would otherwise sit at 0). We reconcile to the exact
4213
+ # server-reported prompt_tokens after the call when it's higher.
4214
+ _step_est = self._estimate_prompt_tokens(args, kwargs)
4215
+ if _step_est > 0:
4216
+ self._code_tokens_total += _step_est
4217
+ if _step_est > self._code_tokens_max:
4218
+ self._code_tokens_max = _step_est
4219
+ _emit({"type": "tokens", "codeInput": self._code_tokens_total,
4220
+ "codeMax": self._code_tokens_max, "stepInput": _step_est})
4221
+
4185
4222
  def _heartbeat():
4186
4223
  waited = 0
4187
4224
  while not hb_stop.wait(45):
@@ -4226,20 +4263,21 @@ def run_agent_chat(cfg):
4226
4263
  raise last_err
4227
4264
  finally:
4228
4265
  hb_stop.set()
4229
- # Task #83: tally PROMPT tokens sent to the code model this step, then emit
4230
- # the running total so the worker can persist it (Mongo) and the REPL can
4231
- # show it live. Best-effort never let accounting break the run.
4266
+ # Task #83: reconcile the pre-call estimate with the server's EXACT
4267
+ # prompt_tokens when it's higher — keeps the running total monotonic (the
4268
+ # estimate already showed the count climbing at request time). Best-effort.
4232
4269
  try:
4233
4270
  tu = getattr(msg, "token_usage", None)
4234
- step_in = int(getattr(tu, "input_tokens", 0) or 0) if tu else 0
4235
- if step_in <= 0:
4236
- step_in = self._estimate_prompt_tokens(args, kwargs)
4237
- if step_in > 0:
4238
- self._code_tokens_total += step_in
4271
+ exact = int(getattr(tu, "input_tokens", 0) or 0) if tu else 0
4272
+ if exact > _step_est:
4273
+ self._code_tokens_total += (exact - _step_est)
4274
+ if exact > self._code_tokens_max:
4275
+ self._code_tokens_max = exact
4239
4276
  _emit({
4240
4277
  "type": "tokens",
4241
4278
  "codeInput": self._code_tokens_total,
4242
- "stepInput": step_in,
4279
+ "codeMax": self._code_tokens_max,
4280
+ "stepInput": exact,
4243
4281
  })
4244
4282
  except Exception: # noqa: BLE001
4245
4283
  pass
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.252",
3
+ "version": "1.0.254",
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",