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 +1 -1
- localcode/agent/recovery.py +21 -21
- localcode/runtime.py +41 -2
- localcode/server_manager.py +36 -33
- {localcode-0.3.3.dist-info → localcode-0.3.4.dist-info}/METADATA +1 -1
- {localcode-0.3.3.dist-info → localcode-0.3.4.dist-info}/RECORD +10 -10
- {localcode-0.3.3.dist-info → localcode-0.3.4.dist-info}/WHEEL +0 -0
- {localcode-0.3.3.dist-info → localcode-0.3.4.dist-info}/entry_points.txt +0 -0
- {localcode-0.3.3.dist-info → localcode-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {localcode-0.3.3.dist-info → localcode-0.3.4.dist-info}/top_level.txt +0 -0
localcode/__init__.py
CHANGED
localcode/agent/recovery.py
CHANGED
|
@@ -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:
|
|
371
|
-
"
|
|
372
|
-
"the
|
|
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}`
|
|
379
|
-
"
|
|
380
|
-
"
|
|
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:
|
|
386
|
-
"
|
|
387
|
-
"
|
|
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:
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
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
|
|
2084
|
-
|
|
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",
|
localcode/server_manager.py
CHANGED
|
@@ -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
|
|
75
|
-
#
|
|
76
|
-
#
|
|
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
|
|
181
|
-
#
|
|
182
|
-
#
|
|
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
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
#
|
|
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
|
|
444
|
-
#
|
|
445
|
-
#
|
|
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
|
|
471
|
-
#
|
|
472
|
-
#
|
|
473
|
-
#
|
|
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,4 +1,4 @@
|
|
|
1
|
-
localcode/__init__.py,sha256=
|
|
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=
|
|
49
|
+
localcode/runtime.py,sha256=UifjZAe8kPgnMCk4N2V2eX4VY6Us6IYmDwbvVwMhwC0,115816
|
|
50
50
|
localcode/runtime_diffusion.py,sha256=bVopikSdQeaV9mAuzWYoXtoXpUb7U0yMXU5VFTpbsBc,39741
|
|
51
|
-
localcode/server_manager.py,sha256=
|
|
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=
|
|
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.
|
|
135
|
-
localcode-0.3.
|
|
136
|
-
localcode-0.3.
|
|
137
|
-
localcode-0.3.
|
|
138
|
-
localcode-0.3.
|
|
139
|
-
localcode-0.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|