localcode 0.3.4__py3-none-any.whl → 0.3.5__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.4"
3
+ __version__ = "0.3.5"
localcode/agent/loop.py CHANGED
@@ -715,22 +715,22 @@ def run_agent_loop(
715
715
  list(getattr(_tool_exec_state, "files_read", {}) or {}),
716
716
  progress_ledger_budget_chars(_win_tokens),
717
717
  )
718
- if _ledger:
719
- model_messages = list(model_messages) + [
720
- {"role": "user", "content": _ledger}
721
- ]
722
718
  except Exception:
723
- pass
724
- # Inject the working-memory checklist (todo_write tool): the model
725
- # always sees its own plan (done / in-progress / left) so it
726
- # advances instead of repeating. Appended LAST, ephemeral, empty-safe.
719
+ _ledger = ""
720
+ # Working-memory checklist (todo_write): model sees its own plan.
721
+ _todo_note = ""
727
722
  try:
728
723
  from ..tools.todo_write import render_todo_reminder
729
- _todos = list(getattr(app.session, "todos", []) or [])
730
- _todo_note = render_todo_reminder(_todos)
731
- if _todo_note:
724
+ _todo_note = render_todo_reminder(list(getattr(app.session, "todos", []) or []))
725
+ except Exception:
726
+ _todo_note = ""
727
+ # Inject ledger + todo as ONE trailing SYSTEM message (not role:user
728
+ # — as a user turn the model re-greeted every round). Ephemeral.
729
+ try:
730
+ _ctx_block = "\n\n".join(b for b in (_ledger, _todo_note) if b)
731
+ if _ctx_block:
732
732
  model_messages = list(model_messages) + [
733
- {"role": "user", "content": _todo_note}
733
+ {"role": "system", "content": _ctx_block}
734
734
  ]
735
735
  except Exception:
736
736
  pass
@@ -164,9 +164,12 @@ def model_identity_line(model: str) -> str:
164
164
  # to its training priors otherwise answers "I'm Gemma by Google". So: lead
165
165
  # with LocalCode, disclose the model only on request.
166
166
  return (
167
- f"You are LocalCode — that is your name and how you refer to yourself in "
168
- f"conversation (say \"I'm LocalCode\", never lead with the model name). "
169
- f"Do NOT volunteer the underlying model in greetings or ordinary replies. "
167
+ f"Your name is LocalCode (never lead with the underlying model name). "
168
+ f"Do NOT introduce or re-introduce yourself. Never begin a message with "
169
+ f"\"I'm LocalCode\", a greeting, or a restatement of what you're doing "
170
+ f"the user already knows who you are and what the task is. Just continue "
171
+ f"the work: take the next action directly. Only state your name if the "
172
+ f"user explicitly asks who you are. "
170
173
  f"The model currently powering you is \"{friendly}\"; report it as EXACTLY "
171
174
  f"\"{friendly}\" — and only — when the user specifically asks which "
172
175
  f"model / version / quant you are, or asks to name something (a file, "
localcode/memory_guard.py CHANGED
@@ -209,3 +209,61 @@ def recommended_jetsam_limit_mb() -> int:
209
209
  total_mb = 16 * 1024
210
210
  reserve = 3500 if total_mb < 24 * 1024 else 6000
211
211
  return max(2048, total_mb - reserve)
212
+
213
+
214
+ def should_disable_metal_residency(model_path: str, ctx_size: int) -> bool:
215
+ """Whether to set GGML_METAL_NO_RESIDENCY=1 for this launch.
216
+
217
+ Root cause of the recurring mid-stream SIGKILL (-9, no crash report, tens
218
+ of GB of system RAM still free): on Apple Silicon, llama.cpp's Metal
219
+ residency sets keep the model weights + KV cache + compute buffers WIRED
220
+ for ~3 minutes (ggml-metal-device.m). During a long generation the wired
221
+ total grows, and when a decode-step spike crosses the GPU's
222
+ `recommendedMaxWorkingSetSize` (~75% of RAM — a hard ceiling independent of
223
+ free system RAM), the kernel kills the process outright. This is the exact
224
+ bug class in ggml-org/llama.cpp#16646 on the same M4 Max / 128 GB hardware.
225
+
226
+ Setting GGML_METAL_NO_RESIDENCY=1 makes those buffers EVICTABLE instead of
227
+ wired, so the OS pages under pressure rather than killing — turning a hard
228
+ crash into (at worst) mild slowdown, and only when actually near the cap.
229
+
230
+ Gated to at-risk launches so the common small-context fast path is
231
+ untouched: large context (>=128K, where KV + compute-buffer growth is the
232
+ trigger) OR a model whose weights alone are a large fraction of the wired
233
+ cap. No user action, no sudo — we control the spawn env.
234
+ """
235
+ try:
236
+ r = subprocess.run(["sysctl", "-n", "hw.memsize"],
237
+ capture_output=True, text=True, timeout=2)
238
+ total_mb = int(r.stdout.strip()) // (1024 * 1024)
239
+ except Exception:
240
+ return False
241
+ # Metal's wired ceiling is ~75% of physical RAM.
242
+ wired_cap_mb = int(total_mb * 0.75)
243
+ if ctx_size >= 131072:
244
+ return True
245
+ try:
246
+ import os as _os
247
+ model_mb = _os.path.getsize(model_path) // (1024 * 1024)
248
+ except Exception:
249
+ model_mb = 0
250
+ # Weights alone over ~55% of the wired cap → KV + compute will breach it.
251
+ return model_mb > int(wired_cap_mb * 0.55)
252
+
253
+
254
+ def metal_residency_env(model_path: str, cmd: list) -> dict:
255
+ """Return {GGML_METAL_NO_RESIDENCY: '1'} when this launch is at risk of the
256
+ wired-cap SIGKILL, else {}. macOS-only; parses --ctx-size from the command.
257
+ """
258
+ import sys as _sys
259
+ if _sys.platform != "darwin":
260
+ return {}
261
+ ctx = 0
262
+ for i, tok in enumerate(cmd):
263
+ if tok == "--ctx-size" and i + 1 < len(cmd):
264
+ try:
265
+ ctx = int(cmd[i + 1])
266
+ except Exception:
267
+ ctx = 0
268
+ break
269
+ return {"GGML_METAL_NO_RESIDENCY": "1"} if should_disable_metal_residency(model_path, ctx) else {}
@@ -299,12 +299,10 @@ class ServerManager:
299
299
  self._lock = threading.Lock()
300
300
  self._last_exit_code: Optional[int] = None # disconnect diagnostics
301
301
  self._last_death_was_pressure: bool = False
302
- # Idle auto-suspend: track wall-clock of the most recent chat
303
- # activity. A background watchdog thread shuts the server down
304
- # after `_idle_timeout_s` seconds of inactivity to stop the GPU
305
- # from cooking the laptop while the user is reading replies.
306
- # The next chat call transparently respawns via _restart_server.
307
- # 0 disables; default 10 min. Override with env LOCALCODE_IDLE_SUSPEND_S.
302
+ # Idle auto-suspend: a watchdog shuts the server down after
303
+ # `_idle_timeout_s` of inactivity (stops the GPU cooking the laptop
304
+ # while the user reads); the next chat call respawns via
305
+ # _restart_server. 0 disables; default 10 min. Env: LOCALCODE_IDLE_SUSPEND_S.
308
306
  import os as _os
309
307
  try:
310
308
  self._idle_timeout_s: float = float(
@@ -394,10 +392,8 @@ class ServerManager:
394
392
  _prev_model = self._model_path # capture before shutdown clears it
395
393
  self._shutdown_locked()
396
394
  # 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.
395
+ # new one (kernel takes 1-3 s to free wired Metal memory; spawning
396
+ # into that window double-commits). Best-effort: ~11 GB target, 8 s.
401
397
  if had_prior:
402
398
  # Only a genuine model change is "model_switch"; a same-model
403
399
  # relaunch (crash/disconnect recovery) is "server_restart".
@@ -422,6 +418,15 @@ class ServerManager:
422
418
  # AGX driver may read the env at first Metal touch — setting it in
423
419
  # the parent env before spawn is the belt-and-suspenders approach.
424
420
  env.setdefault("AGX_RELAX_CDM_CTXSTORE_TIMEOUT", "1")
421
+ # Disable Metal residency sets on at-risk launches → prevents the wired-cap SIGKILL.
422
+ try:
423
+ from .memory_guard import metal_residency_env
424
+ _renv = metal_residency_env(str(model_path), cmd)
425
+ if _renv:
426
+ env.update(_renv)
427
+ _lifecycle_log("metal_no_residency", model=str(model_path))
428
+ except Exception:
429
+ pass
425
430
  # Capture the server's OWN stdout/stderr to server.log (this runtime
426
431
  # path used to send both to /dev/null, discarding llama-server's
427
432
  # final ggml/Metal error on the recurring -9). Append per-spawn.
@@ -461,11 +466,10 @@ class ServerManager:
461
466
  except Exception:
462
467
  pass
463
468
 
464
- # Layer 2: userspace memory-pressure monitor. Polls
465
- # kern.memorystatus_vm_pressure_level every 500 ms; on WARN
466
- # transition, SIGTERMs our server before the kernel stalls.
467
- # Complementary to the jetsam ceiling acts on system-wide
468
- # pressure (everything else swelling) not just our own RSS.
469
+ # Layer 2: userspace pressure monitor — polls
470
+ # kern.memorystatus_vm_pressure_level every 500 ms and SIGTERMs our
471
+ # server on WARN. Complements the jetsam ceiling (acts on system-
472
+ # wide pressure, not just our own RSS).
469
473
  try:
470
474
  self._pressure_thread = start_pressure_monitor(
471
475
  self._process,
@@ -474,10 +478,8 @@ class ServerManager:
474
478
  except Exception:
475
479
  self._pressure_thread = None
476
480
  self._write_pid_file(self._process.pid)
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.
481
+ # Log the FULL launch command (which flags was the server using?);
482
+ # truncated to 4000 chars (~80 args) to keep the event small.
481
483
  _flags_str = " ".join(str(c) for c in cmd)[:4000]
482
484
  _lifecycle_log("server_started", pid=self._process.pid, port=port,
483
485
  model=Path(model_path).name,
@@ -698,10 +700,8 @@ class ServerManager:
698
700
  _lifecycle_log("pressure_kill", level=level, pid=killed_pid,
699
701
  guard="memory_pressure", exit_code=self._last_exit_code,
700
702
  free_mb=_system_free_memory_mb())
701
- # Clear internal state so subsequent code paths see "no server
702
- # running" rather than a phantom Popen handle. Safe to do from
703
- # the pressure-monitor thread because `_process` reads/writes
704
- # are atomic in CPython for the assignment.
703
+ # Clear internal state so callers see "no server" not a phantom Popen.
704
+ # Safe from the pressure-monitor thread (assignment is atomic in CPython).
705
705
  self._process = None
706
706
  self._model_path = None
707
707
 
@@ -173,6 +173,19 @@ def execute(ctx: ToolContext, args: dict) -> str:
173
173
  # catches the data.py-style "model copies in-memory stub back into
174
174
  # write_file" disaster, which was the one case where rejecting
175
175
  # was load-bearing.
176
+ # Log the EXACT content the model sent (post-JSON-parse) so a "the file is
177
+ # corrupted / write_file isn't writing what I sent" report is provable from
178
+ # logs in one grep, instead of a mystery. Best-effort, capped, append-only.
179
+ try:
180
+ from ..paths import global_state_dir
181
+ _wl = global_state_dir() / "write_content.log"
182
+ with open(_wl, "a", encoding="utf-8", errors="replace") as _fh:
183
+ _fh.write(f"\n===== write_file {args['path']} ({len(content)} chars) =====\n")
184
+ _fh.write(content[:20000])
185
+ _fh.write("\n")
186
+ except Exception:
187
+ pass
188
+
176
189
  existed = path.is_file()
177
190
  # Capture the prior content so an in-place rewrite renders as a diff card
178
191
  # (like edit_file/multi_edit), not a bare "Rewrote (N lines)" one-liner.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localcode
3
- Version: 0.3.4
3
+ Version: 0.3.5
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=W-WadG5QMLaRAlMnY-zGSUOIpCiTp4nDOBUifILPLVU,49
1
+ localcode/__init__.py,sha256=1BYETer-qr6n0hQRQuRG08uTZrtp262X5k87-OyEhp0,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
@@ -30,7 +30,7 @@ localcode/injection_defense.py,sha256=sggmoowHEAKYV3v-1PTWtv8KjekEL0kZ0-O03hb3Dm
30
30
  localcode/launcher.py,sha256=uDsIy66q-VexbvdIb7iwzYwDmmYhWRP10SNX4GiEWQ4,11462
31
31
  localcode/logging_utils.py,sha256=mFn_LO26IkT7OLxKs1ke4mND8BJeDLf-UULCYSSxyz0,920
32
32
  localcode/lsp.py,sha256=WTtdbdgforZdWMRqlzCQp2ndkKRbfIE1f5GfGeRH1xU,5939
33
- localcode/memory_guard.py,sha256=Y4wUlPdV4zRzwkNmPVbCJCIBf40j_OrQ0rc7xGXGoNs,8815
33
+ localcode/memory_guard.py,sha256=9OBpvzOGP_gzYvG3sXhiYr6vzTJRW_YtRDaccouSIPU,11390
34
34
  localcode/model_config.py,sha256=4iPw5M6tnjMvdILtWlRkOdE6QMFRJCAIYBmadc82ZjQ,14261
35
35
  localcode/model_families.py,sha256=mEBIoEMe-qL93Twxm-4mYv76dWEkKVp5CiQlY43mkWA,11203
36
36
  localcode/models.py,sha256=fetDQSmOR3GrMMv_H9ecAZSmHa2TPhsYw8eSYKDl5YA,8243
@@ -48,7 +48,7 @@ localcode/recommendations.py,sha256=HVh8u2L3xbwc2qxH7Syec2cEtlEoFV9n1c33SqWF3ng,
48
48
  localcode/recovery.py,sha256=QUSsBA9IS5AJ6W4m4nIv2RaktTO9TJeL0RcDeUSWHJk,5237
49
49
  localcode/runtime.py,sha256=UifjZAe8kPgnMCk4N2V2eX4VY6Us6IYmDwbvVwMhwC0,115816
50
50
  localcode/runtime_diffusion.py,sha256=bVopikSdQeaV9mAuzWYoXtoXpUb7U0yMXU5VFTpbsBc,39741
51
- localcode/server_manager.py,sha256=HV_1VQxJrN_Vvjrttn9Nr9OQHYfDNmWoCzZO8JdFIp8,34488
51
+ localcode/server_manager.py,sha256=9xHqShGB9rVr62qhTwWkYZDX4KOuxACvujhFC0ztYdM,34305
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
@@ -71,9 +71,9 @@ localcode/agent/context.py,sha256=adV_WlFBHMDHsWdndRgrkZqRweDD9plyqMP_2X74WAw,37
71
71
  localcode/agent/goal.py,sha256=X87Q3rsdp70wOfkpbh35M4qFWSNyklX3RuRWjv-pCXs,6264
72
72
  localcode/agent/helpers.py,sha256=vutFyCtnca0UnF0wgk7LdsqVWdiqyYm4YnFOxtnCJ_A,15004
73
73
  localcode/agent/hooks.py,sha256=XadEbSM7gHtKlLrqSKnvcQFcoWoNUXdlMdIPS-EwBGA,15841
74
- localcode/agent/loop.py,sha256=ZX3VS_7jCcRLX3VCSOsl8uMF51AnPICt18awXGk928s,99869
74
+ localcode/agent/loop.py,sha256=2O2iKFsCQmr1CoQq40Ls_NeBs7tFgcDVfkU8YhqXXdM,99864
75
75
  localcode/agent/prompt_context.py,sha256=abJPLMhTzuVcsW4AfMBIW-SEqmrU69DhI-0vGZllZVU,13707
76
- localcode/agent/prompts.py,sha256=1VFrtzumZ2A7BaZ1gStpre-mZBs721FZi9hiB4qenZw,14639
76
+ localcode/agent/prompts.py,sha256=LCJBM2mV_SKc1OXtH0-ih9OPQnhrTh3vTlrOgLUiVDA,14845
77
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
@@ -114,7 +114,7 @@ localcode/tools/skill_tool.py,sha256=vTVPyRWxICtDCVYWMiAMInGnc-wGAk2L1EZn-6e79nE
114
114
  localcode/tools/todo_write.py,sha256=8A4k9MzoqOtzJ6y2Gd3lEGkINcpV3JVCd2lYBmdd6qI,5801
115
115
  localcode/tools/web_fetch.py,sha256=UPhMrLipwN5xJx5-zr634gL4p6nIRqYI2_7tbUw_8Go,2770
116
116
  localcode/tools/web_search.py,sha256=ILfBBx50p00OjA939HzdDmdao4QLL8sRuxlBoUDjmC8,1122
117
- localcode/tools/write_file.py,sha256=zsG2QDkofjjqaUte1mio9_0BvdXpr-LIrNBD1U-pp9Y,9129
117
+ localcode/tools/write_file.py,sha256=2rfbp0j_ApHuk8fUAopMl6H1BIyEMKWKXjZ__xrY6oI,9743
118
118
  localcode/tui/__init__.py,sha256=Yfy9bvw3l1iY4FDhRX8qZVgnZLuYiq8lc9SDYl4cWwc,61
119
119
  localcode/tui/app.py,sha256=KjQzpGKnxkgh-0kVc3zcb63Q6a5LBuGreBqTUrasiqM,17700
120
120
  localcode/tui/bridge.py,sha256=urPPCqC_nisAYawWk3juvMI8IaoJfv4g7USpGOZOqQc,2803
@@ -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.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,,
134
+ localcode-0.3.5.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
135
+ localcode-0.3.5.dist-info/METADATA,sha256=F80qNJ73o16hDwmaZzV6e9B-EGuc3ARhiHgwg_P0HDo,7813
136
+ localcode-0.3.5.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
137
+ localcode-0.3.5.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
138
+ localcode-0.3.5.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
139
+ localcode-0.3.5.dist-info/RECORD,,