localcode 0.3.3__py3-none-any.whl → 0.3.4__py3-none-any.whl

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.
localcode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.3"
3
+ __version__ = "0.3.4"
@@ -365,36 +365,36 @@ def churn_nudge_for(signal: ChurnSignal) -> str:
365
365
  Names the concrete subject (file path / command) + count so the model
366
366
  sees what it's thrashing on, then tells it the ONE thing to do instead.
367
367
  """
368
+ # NOTE: these are FORWARD-ONLY imperatives. They deliberately contain NO
369
+ # self-referential loop language ("you're going in circles", "you've
370
+ # rewritten X N times", "thrashing", "stop planning"). A small model
371
+ # parrots whatever we inject: injecting "you're going in circles" as a
372
+ # user-role message makes it reply "You're right, I've been going in
373
+ # circles" — which then sits in its own context and self-conditions the
374
+ # loop deeper (arXiv:2509.09677). So we tell it the NEXT concrete action
375
+ # and nothing about the failure it just had.
368
376
  if signal.mode is ChurnMode.FILE_REWRITE:
369
377
  return (
370
- f"SYSTEM: You've rewritten {signal.subject} {signal.count} times "
371
- "this turn. Stop rewriting it. Read the ACTUAL error output from "
372
- "the last failure, identify the specific line/cause, and make ONE "
373
- "targeted edit_file change to fix that — do not overwrite the whole "
374
- "file again."
378
+ f"SYSTEM: {signal.subject} is written — move on to the next file. "
379
+ "If it has a specific error, make ONE targeted edit_file change to "
380
+ "the exact line; otherwise create the next file the task needs."
375
381
  )
376
382
  if signal.mode is ChurnMode.COMMAND_FAILURE:
377
383
  return (
378
- f"SYSTEM: `{signal.subject}` has failed {signal.count} times this "
379
- "turn. Re-running it will not help. Read its error output line by "
380
- "line, fix the ROOT CAUSE (a missing dependency, a syntax error in "
381
- "a config/source file, a wrong path), and only then run it again."
384
+ f"SYSTEM: `{signal.subject}` won't succeed as-is. Read its error "
385
+ "output, fix the root cause (missing dependency, a syntax error in a "
386
+ "config/source file, a wrong path) with an edit, then run it once."
382
387
  )
383
388
  if signal.mode is ChurnMode.PLANNING_SPIN:
384
389
  return (
385
- "SYSTEM: You've spent several rounds planning and re-deriving the "
386
- "same approach without changing a single file or running a build. "
387
- "You've planned enough. Take ONE concrete action NOW: create or "
388
- "edit the most important file for the next step, then build/run to "
389
- "verify it. Do not restate the plan — execute the first step of it."
390
+ "SYSTEM: Take the next concrete action now create or edit the next "
391
+ "file the task needs, then run the build to verify. Write code, not "
392
+ "a plan."
390
393
  )
391
394
  # INVESTIGATION_SPIN
392
395
  return (
393
- "SYSTEM: Several rounds of reading/listing/searching without WRITING "
394
- "anything you're going in circles. STOP exploring. If the task is to "
395
- "build something, create the target file and write real code THIS round "
396
- "with write_file/edit_file; do NOT read, ls, find, cat, or grep again. "
397
- "Leftover files on disk are NOT your progress (may be an unrelated run) — "
398
- "only files YOU write count. If you truly lack info only the user has, "
399
- "ask ONE focused question; otherwise start writing now."
396
+ "SYSTEM: Take a concrete action now create the next file the task "
397
+ "needs and write real code with write_file/edit_file. Files already on "
398
+ "disk that you did not create are not part of your task. If you truly "
399
+ "lack information only the user has, ask ONE focused question."
400
400
  )
localcode/runtime.py CHANGED
@@ -168,6 +168,21 @@ def _log_disconnect_context(stage: str, error: Any, *, mid_stream: bool = False)
168
168
  diag["stage"] = stage
169
169
  diag["mid_stream"] = mid_stream
170
170
  diag["error"] = err_str[:500]
171
+ # Capture the SERVER's own last words. llama-server prints the real cause
172
+ # (ggml/Metal allocation failure, GGML_ASSERT, OOM) to server.log right
173
+ # before it dies; without this we only see httpx's "connection dropped"
174
+ # and are left guessing. The tail is the actual diagnosis.
175
+ _server_tail = ""
176
+ try:
177
+ from .paths import global_state_dir
178
+ _lp = global_state_dir() / "server.log"
179
+ if _lp.exists():
180
+ _lines = _lp.read_text(errors="replace").splitlines()
181
+ _server_tail = "\n".join(_lines[-15:])[-1500:]
182
+ except Exception:
183
+ _server_tail = ""
184
+ if _server_tail:
185
+ diag["server_log_tail"] = _server_tail
171
186
  try:
172
187
  from .server_manager import _lifecycle_log as _ll
173
188
  _ll("server_disconnect", **diag)
@@ -186,6 +201,8 @@ def _log_disconnect_context(stage: str, error: Any, *, mid_stream: bool = False)
186
201
  f"pressure_kill={diag.get('pressure_kill')} "
187
202
  f"running={diag.get('running')} mid_stream={mid_stream} "
188
203
  f"free_mb={diag.get('free_mb')} error={err_str[:300]}\n"
204
+ + (f" server.log tail:\n "
205
+ + _server_tail.replace("\n", "\n ") + "\n" if _server_tail else "")
189
206
  )
190
207
  except Exception:
191
208
  pass
@@ -2064,6 +2081,25 @@ class LocalCodeRuntimeGateway(_DiffusionMixin):
2064
2081
  # revert this hunk and accept occasional rephrase as a
2065
2082
  # model property of Qwen3.6 IQ2_M.
2066
2083
  payload["repeat_penalty"] = 1.05
2084
+ # Forward the EOS-NEUTRAL anti-loop samplers that were previously
2085
+ # computed into `opts` and then DROPPED (they only went to the dead
2086
+ # ollama-shaped payload below). DRY penalises repeated N-GRAMS —
2087
+ # not single tokens — so unlike repeat_penalty it does NOT suppress
2088
+ # the EOS/sentence-end tokens that caused the 2026-04-29 paraphrase
2089
+ # regression; min_p trims the low-probability tail that token-
2090
+ # collapse loops feed on. llama-server has DRY OFF by default, so
2091
+ # NOT forwarding these is a real root cause of the repeat-collapse
2092
+ # loops. We deliberately do NOT re-add the aggressive repeat_penalty
2093
+ # /top_k/top_p bundle (that was the regression). A/B back to
2094
+ # temperature-only with LOCALCODE_SAMPLER_MINIMAL=1.
2095
+ import os as _os
2096
+ if _os.environ.get("LOCALCODE_SAMPLER_MINIMAL") != "1":
2097
+ payload["dry_multiplier"] = opts["dry_multiplier"]
2098
+ payload["dry_base"] = opts["dry_base"]
2099
+ payload["dry_allowed_length"] = opts["dry_allowed_length"]
2100
+ payload["dry_penalty_last_n"] = opts["dry_penalty_last_n"]
2101
+ if opts.get("min_p", 0):
2102
+ payload["min_p"] = opts["min_p"]
2067
2103
  if "num_predict" in opts:
2068
2104
  _np = opts["num_predict"]
2069
2105
  # llama-server treats max_tokens=-1 as "use default", which on
@@ -2080,8 +2116,11 @@ class LocalCodeRuntimeGateway(_DiffusionMixin):
2080
2116
  # outbound chat-completions payload so we can verify DRY is
2081
2117
  # actually being forwarded. Set LOCALCODE_DEBUG_SAMPLERS=1
2082
2118
  # to enable. Self-disables after one log to keep noise down.
2083
- import os, logging
2084
- if os.environ.get("LOCALCODE_DEBUG_SAMPLERS") == "1" and not getattr(self, "_logged_samplers", False):
2119
+ import logging
2120
+ # Always log the ACTUAL forwarded samplers once per session (was
2121
+ # opt-in) so we can correlate looping with the real server config —
2122
+ # the "control this better" record. Cheap: fires exactly once.
2123
+ if not getattr(self, "_logged_samplers", False):
2085
2124
  sampler_keys = (
2086
2125
  "temperature", "top_p", "top_k", "min_p",
2087
2126
  "repeat_penalty", "repeat_last_n",
@@ -71,12 +71,9 @@ from .paths import (
71
71
  lifecycle_log_path,
72
72
  )
73
73
 
74
- # PID file stays GLOBAL — only one llama-server runs on the machine
75
- # (single port, single GPU memory budget). When you `cd` between
76
- # projects mid-session, the running server stays serving you. The
77
- # per-project lifecycle log records a fresh `server_started` entry
78
- # on next launch from the new project. See paths.py for the full
79
- # global-vs-project split rationale.
74
+ # PID file stays GLOBAL — only one llama-server runs per machine (single port,
75
+ # single GPU budget), so it keeps serving you when you `cd` between projects.
76
+ # See paths.py for the global-vs-project split rationale.
80
77
  PID_FILE = server_pid_file()
81
78
  DEFAULT_PORT = 8081
82
79
  # Preferred-range scan (8081-8099) before falling through to an OS-assigned
@@ -177,13 +174,9 @@ def _rewrite_port_arg(cmd: list[str], port: int) -> list[str]:
177
174
 
178
175
  STUCK_SERVER_MARKER = stuck_server_marker_path()
179
176
 
180
- # Persistent (append-only) lifecycle log for every server start, stop,
181
- # pressure-kill, and recovery event. Distinct from the per-session
182
- # `~/.local/share/localcode/server.log` (which the setup screen
183
- # overwrites with `open(..., "w")` on every launch and so loses
184
- # inter-session context). Per-project (lives at
185
- # `<project_root>/.localcode/lifecycle.log`) so each project has its
186
- # own diagnostic timeline.
177
+ # Persistent (append-only) lifecycle log for every server start/stop/kill/
178
+ # recovery event. Per-project (`<project_root>/.localcode/lifecycle.log`) so
179
+ # each project keeps its own diagnostic timeline across sessions.
187
180
  LIFECYCLE_LOG = lifecycle_log_path()
188
181
 
189
182
 
@@ -400,12 +393,11 @@ class ServerManager:
400
393
  had_prior = self._process is not None
401
394
  _prev_model = self._model_path # capture before shutdown clears it
402
395
  self._shutdown_locked()
403
- # Wait for the OLD server's memory to actually release before
404
- # spawning the new one: the kernel may take 1-3 s to free wired
405
- # Metal memory after the child reaps, and spawning the new ~10 GB
406
- # server in that window double-commits and trips the pressure
407
- # monitor on a 16 GB Mac. Best-effort targets ~11 GB free, times
408
- # out at 8 s, and proceeds anyway (refusing would be worse).
396
+ # Wait for the OLD server's memory to release before spawning the
397
+ # new one: the kernel takes 1-3 s to free wired Metal memory after
398
+ # the child reaps, and spawning into that window double-commits and
399
+ # trips the pressure monitor on a 16 GB Mac. Best-effort — targets
400
+ # ~11 GB free, times out at 8 s, proceeds anyway.
409
401
  if had_prior:
410
402
  # Only a genuine model change is "model_switch"; a same-model
411
403
  # relaunch (crash/disconnect recovery) is "server_restart".
@@ -430,20 +422,35 @@ class ServerManager:
430
422
  # AGX driver may read the env at first Metal touch — setting it in
431
423
  # the parent env before spawn is the belt-and-suspenders approach.
432
424
  env.setdefault("AGX_RELAX_CDM_CTXSTORE_TIMEOUT", "1")
425
+ # Capture the server's OWN stdout/stderr to server.log (this runtime
426
+ # path used to send both to /dev/null, discarding llama-server's
427
+ # final ggml/Metal error on the recurring -9). Append per-spawn.
428
+ log_fh = None
429
+ try:
430
+ from .paths import global_state_dir
431
+ log_fh = open(global_state_dir() / "server.log", "a", buffering=1)
432
+ log_fh.write(f"\n===== llama-server spawn (port {port}) =====\n")
433
+ except Exception:
434
+ log_fh = None
435
+ try: # close any prior spawn's handle before replacing it
436
+ if getattr(self, "_log_fh", None) is not None:
437
+ self._log_fh.close()
438
+ except Exception:
439
+ pass
440
+ self._log_fh = log_fh
433
441
  self._process = subprocess.Popen(
434
442
  cmd,
435
- stdout=subprocess.DEVNULL,
436
- stderr=subprocess.DEVNULL,
443
+ stdout=(log_fh or subprocess.DEVNULL),
444
+ stderr=subprocess.STDOUT if log_fh else subprocess.DEVNULL,
437
445
  start_new_session=True, # process group leader → killpg works
438
446
  env=env,
439
447
  )
440
448
  self._model_path = model_path
441
449
  self._port = port
442
450
 
443
- # Layer 1: kernel-enforced memory ceiling via jetsam. If
444
- # llama-server tries to wire more than the budget allows,
445
- # macOS kernel kills it BEFORE the vm_fault wait-chain can
446
- # form. This is the hard backstop — sanctioned Apple API.
451
+ # Layer 1: kernel-enforced memory ceiling via jetsam — the hard
452
+ # backstop (sanctioned Apple API) that kills llama-server before it
453
+ # can push the system over the edge.
447
454
  try:
448
455
  from .memory_guard import (
449
456
  set_jetsam_highwater, recommended_jetsam_limit_mb,
@@ -467,14 +474,10 @@ class ServerManager:
467
474
  except Exception:
468
475
  self._pressure_thread = None
469
476
  self._write_pid_file(self._process.pid)
470
- # Log the FULL command we launched with so future debugging
471
- # can answer "what flags was the server using during this
472
- # session?" needed because subtle flags like
473
- # `--lookup-cache-dynamic` or `--spec-type ngram-mod` cause
474
- # repetition pathologies that look like model bugs but are
475
- # actually decode-time speculative-cache feedback loops.
476
- # Truncated to 4000 chars to keep the event small; that's
477
- # enough for ~80 args.
477
+ # Log the FULL launch command so debugging can answer "what flags
478
+ # was the server using?" subtle flags (e.g. --spec-type ngram-mod)
479
+ # cause repetition pathologies that look like model bugs. Truncated
480
+ # to 4000 chars (~80 args) to keep the event small.
478
481
  _flags_str = " ".join(str(c) for c in cmd)[:4000]
479
482
  _lifecycle_log("server_started", pid=self._process.pid, port=port,
480
483
  model=Path(model_path).name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localcode
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: High-performance AI coding on consumer hardware.
5
5
  Author: LocalCode contributors
6
6
  License-Expression: Apache-2.0
@@ -1,4 +1,4 @@
1
- localcode/__init__.py,sha256=bQESHtF45jqmHUASEOi1qA8k5Ze3-o_Uh926z32yitM,49
1
+ localcode/__init__.py,sha256=W-WadG5QMLaRAlMnY-zGSUOIpCiTp4nDOBUifILPLVU,49
2
2
  localcode/__main__.py,sha256=_4Tblte9F1KOpB-oGBQuTlaSN-IncLmhihqhBkpH3pU,69
3
3
  localcode/_subproc_env.py,sha256=eltq_kKvqGj22pIFivB5BZft9-s5fziBW0lsV9FhEV4,2383
4
4
  localcode/app.py,sha256=FSwpce6Jfn7zJmhL1ipaqTq0xKh0MxCMnEnMQuFV8L8,67820
@@ -46,9 +46,9 @@ localcode/plans.py,sha256=Tih7WzRmkWO0cF4fEDwjI07978mJZ_l9jXTSTidLb9k,7424
46
46
  localcode/process_registry.py,sha256=KBd5ndChtbmIEIFSkGTmAtaLEIIAzc8DhuYX692q73Y,5165
47
47
  localcode/recommendations.py,sha256=HVh8u2L3xbwc2qxH7Syec2cEtlEoFV9n1c33SqWF3ng,3184
48
48
  localcode/recovery.py,sha256=QUSsBA9IS5AJ6W4m4nIv2RaktTO9TJeL0RcDeUSWHJk,5237
49
- localcode/runtime.py,sha256=7BWAf-PaUvP0SUBFzxLB4edb6FcLOZay_sBafAPLDIk,113508
49
+ localcode/runtime.py,sha256=UifjZAe8kPgnMCk4N2V2eX4VY6Us6IYmDwbvVwMhwC0,115816
50
50
  localcode/runtime_diffusion.py,sha256=bVopikSdQeaV9mAuzWYoXtoXpUb7U0yMXU5VFTpbsBc,39741
51
- localcode/server_manager.py,sha256=DLXnijvGfWbW3tGNaj0sEWIQVJ0N7Jadl_YIWc4W1RE,34339
51
+ localcode/server_manager.py,sha256=HV_1VQxJrN_Vvjrttn9Nr9OQHYfDNmWoCzZO8JdFIp8,34488
52
52
  localcode/session.py,sha256=taEyOOiCcNXEr8oOeszyWvzvn-oAZ7knXQ7BxRew08Q,11970
53
53
  localcode/shell.py,sha256=2QcXk0NB6d8t2gpbbKOB5nGZ_xBJ7VVsLtF4hnUknVM,2300
54
54
  localcode/skills.py,sha256=TGdy63dQTlfa6U2l7wjg3zSatKyLAYdu_wVO-J2BrHY,25800
@@ -74,7 +74,7 @@ localcode/agent/hooks.py,sha256=XadEbSM7gHtKlLrqSKnvcQFcoWoNUXdlMdIPS-EwBGA,1584
74
74
  localcode/agent/loop.py,sha256=ZX3VS_7jCcRLX3VCSOsl8uMF51AnPICt18awXGk928s,99869
75
75
  localcode/agent/prompt_context.py,sha256=abJPLMhTzuVcsW4AfMBIW-SEqmrU69DhI-0vGZllZVU,13707
76
76
  localcode/agent/prompts.py,sha256=1VFrtzumZ2A7BaZ1gStpre-mZBs721FZi9hiB4qenZw,14639
77
- localcode/agent/recovery.py,sha256=AA2qQwsmlgB_RV_JvQVwURj1xTxQa2c-34SljnFZeHk,16806
77
+ localcode/agent/recovery.py,sha256=oCghDm9BdHhvl98Kx8EikNuxSk4Geh-0cgAEhTpH-sw,16738
78
78
  localcode/agent/sections.py,sha256=9hnwaNritQHgn7qIGFBZ3ChZgMQXQcqtmpdbR1wafA0,8998
79
79
  localcode/agent/streaming.py,sha256=TEcKbmKfDGV1razrV-ceVrvIIxvS-GmLWGNDnF0gSTA,12872
80
80
  localcode/agent/tool_execution.py,sha256=yfWZwOO3f5ICd2eqw6keg8sOKOIa3vFt4peFinmu5nM,14462
@@ -131,9 +131,9 @@ localcode/tui/widgets/chat_log.py,sha256=D5DUkrQXL_ilxyC8N1xIPLw4Pqeh0jSWt1mPgho
131
131
  localcode/tui/widgets/voice_visualizer.py,sha256=zOhrIxaMcg-F2Y8LvHJTddq227phmiTjtEyRbkx1XxU,4709
132
132
  localcode/tui/widgets/messages/__init__.py,sha256=952AZ1qVMGUIqPIry7mwxnYbMkgPlcY5FAwZtEa5YPg,967
133
133
  localcode/tui/widgets/messages/diff.py,sha256=khDf_MThrQz0S62-t-m7i3OU5cdNolUk1TCeKMRPDQI,10910
134
- localcode-0.3.3.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
135
- localcode-0.3.3.dist-info/METADATA,sha256=22UXuP5QSAS0stB-2Nb7uUKNUlwKJPnnN7CnWmo6v2Y,7813
136
- localcode-0.3.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
137
- localcode-0.3.3.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
138
- localcode-0.3.3.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
139
- localcode-0.3.3.dist-info/RECORD,,
134
+ localcode-0.3.4.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
135
+ localcode-0.3.4.dist-info/METADATA,sha256=AJAQv0DAC-RtSDeszeoSwXHacCrJRNKPODmpWOurUjs,7813
136
+ localcode-0.3.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
137
+ localcode-0.3.4.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
138
+ localcode-0.3.4.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
139
+ localcode-0.3.4.dist-info/RECORD,,