code-context-control 2.118.0__py3-none-any.whl → 2.118.2__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.
- cli/c3.py +1 -1
- cli/hub_server.py +5 -3
- {code_context_control-2.118.0.dist-info → code_context_control-2.118.2.dist-info}/METADATA +1 -1
- {code_context_control-2.118.0.dist-info → code_context_control-2.118.2.dist-info}/RECORD +13 -12
- oracle/services/mobile_api.py +6 -4
- services/access_guard.py +13 -5
- services/agent_locks.py +10 -4
- services/atomic_json.py +118 -0
- services/shell_output.py +17 -3
- {code_context_control-2.118.0.dist-info → code_context_control-2.118.2.dist-info}/WHEEL +0 -0
- {code_context_control-2.118.0.dist-info → code_context_control-2.118.2.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.118.0.dist-info → code_context_control-2.118.2.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.118.0.dist-info → code_context_control-2.118.2.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
cli/hub_server.py
CHANGED
|
@@ -32,6 +32,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
32
32
|
from core.ide import PROFILES, detect_ide, get_profile, load_ide_config, normalize_ide_name
|
|
33
33
|
from services import access_guard
|
|
34
34
|
from services.activity_log import ActivityLog
|
|
35
|
+
from services.atomic_json import write_json_atomic
|
|
35
36
|
from services.project_manager import ProjectManager
|
|
36
37
|
from services.tool_classifier import CATEGORIES
|
|
37
38
|
|
|
@@ -3078,9 +3079,10 @@ def api_projects_config_put():
|
|
|
3078
3079
|
else:
|
|
3079
3080
|
merged[key] = val
|
|
3080
3081
|
cfg[section] = merged
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3082
|
+
# Not `config.json.tmp`: the CLI (services.access_guard) and the mobile
|
|
3083
|
+
# API write this same file, and the hub itself is threaded — one shared
|
|
3084
|
+
# temp name is a name they all pick at once. See services.atomic_json.
|
|
3085
|
+
write_json_atomic(cfg_file, cfg, trailing_newline=False)
|
|
3084
3086
|
try:
|
|
3085
3087
|
ActivityLog(str(resolved)).log("hub_config_write", {
|
|
3086
3088
|
"section": section, "keys": sorted(values.keys()), "source": "hub"})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.118.
|
|
3
|
+
Version: 2.118.2
|
|
4
4
|
Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
|
|
2
2
|
cli/_hook_utils.py,sha256=lxYVHSqK4FTiFU2avXW2hbOheMbaaZhnAZqqwqtrZHI,20862
|
|
3
3
|
cli/_shell_writes.py,sha256=XzUNlUDljmg-wZxK03xHnRe36WeQxe4tpO9HKVFwP5o,5090
|
|
4
|
-
cli/c3.py,sha256=
|
|
4
|
+
cli/c3.py,sha256=7qtAuI6yD-23IZoP5aq78nG8lAdZ1FEQ7if9wcfplAc,417916
|
|
5
5
|
cli/docs.html,sha256=X9M7qC7LWi8pPJNF-4yEM57jhuZ3WeTESECbdDOeNaA,143172
|
|
6
6
|
cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
|
|
7
7
|
cli/hook_access_guard.py,sha256=ba8-un6G9Ap8HCzx7nyhVq3c_G4sOtiVXRy0BzzX8-Y,23349
|
|
@@ -19,7 +19,7 @@ cli/hook_prompt_recall.py,sha256=LHyubu36pf5h3hbLBe0yH-yGhAlwLJdsc7hfa7WB1h4,347
|
|
|
19
19
|
cli/hook_read.py,sha256=ZWZldgqGSNz78COhlgETQ7yxdOW5vWIIHUet9gZKhxk,8176
|
|
20
20
|
cli/hook_session_stats.py,sha256=NbB6_JqUjC2HBfWQTyVLibTqB74s89dhUDvTG6APvcM,5479
|
|
21
21
|
cli/hook_terse_advisor.py,sha256=W_zaTfX2akgX-ifuUTsLu2H1atM8XmWt6jpPoeU7Pm0,6252
|
|
22
|
-
cli/hub_server.py,sha256=
|
|
22
|
+
cli/hub_server.py,sha256=4BxvRHCGNc87jOtWankOVjQLYNk-GhWI2OY1harC8ro,184548
|
|
23
23
|
cli/hub_ui.html,sha256=nnGqWhWGCLfNisGUnoAlQFcBr6wPNsq-AEOR7uFtVkE,2942
|
|
24
24
|
cli/mcp_proxy.py,sha256=92htuT-p0j-cDTbyqlIJpGoQ85_Aw7UuB8L_Toi_u20,17511
|
|
25
25
|
cli/mcp_server.py,sha256=ZkYkCAchuxEaVyU4kxOQHmQj5xDy_Sj4JElCQmrp2VY,64517
|
|
@@ -122,7 +122,7 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
|
|
|
122
122
|
cli/ui/components/settings.js,sha256=ATbAjBlVIwCNpxq7s191b49a_INQV38iwmySqtJLYwY,79066
|
|
123
123
|
cli/ui/components/sidebar.js,sha256=K2ym2kUgpbyG-EBA_wBIIiqQY8qTatsiBZwzseMWuIQ,9939
|
|
124
124
|
cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
|
|
125
|
-
code_context_control-2.118.
|
|
125
|
+
code_context_control-2.118.2.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
126
126
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
127
127
|
core/config.py,sha256=YmkcZwedz_lfDM0ZuI_f4xpe98ixPLcSQFcTCHgRiRg,19206
|
|
128
128
|
core/ide.py,sha256=V6VVMVsFdmmcsMyxikjQp7z9xa42CWiHKS-ya-MAcG4,6172
|
|
@@ -148,7 +148,7 @@ oracle/services/insight_engine.py,sha256=lwcdXtjt3CSZLqHVpXx6SG5gDhZYUKwXt4Z8dGy
|
|
|
148
148
|
oracle/services/local_session.py,sha256=nkawOjDbotUp5wtZQnC1wPDw3HopNYMkoYqWpP5ToCU,9227
|
|
149
149
|
oracle/services/memory_reader.py,sha256=rSEmoIXQu2JkolHJIQWpn0piPJpyuf1KZvW_9liQwkU,3727
|
|
150
150
|
oracle/services/memory_writer.py,sha256=hpvYsxjhl293jEjGcRKfjG5njUaAOXKqbmJaUu1WdUU,6997
|
|
151
|
-
oracle/services/mobile_api.py,sha256=
|
|
151
|
+
oracle/services/mobile_api.py,sha256=9nYE84FRHtgiwv6b9Hs17o3pKYXtPlYCn1Xny_gMLhk,120326
|
|
152
152
|
oracle/services/project_scanner.py,sha256=PkYJtwOLRME1EzwRy3GIkYk6Xw0u1_BzFcaMJ5gJ1_s,6118
|
|
153
153
|
oracle/services/review_agent.py,sha256=m_fzoHowpiibf0_iz3dF72LWSrwB_FbDd1am-SbeWXg,11273
|
|
154
154
|
oracle/services/tool_executor.py,sha256=xtAkBWkclh_FwOgzprjGkyRIV8-A7Lc3bz0UFPShrv0,1113
|
|
@@ -173,14 +173,15 @@ oracle/ui/chat/send.js,sha256=X_Uc6EuuIxkUTlRWGGYOq6gQkJCQeCE3p5NMyxx7RzQ,11911
|
|
|
173
173
|
oracle/ui/chat/stream_renderer.js,sha256=RZndI2mu5xbzZF-4NG9jeOqXTcmIqfZqxgkrBvTzWmk,29861
|
|
174
174
|
oracle/ui/chat/toolbar.js,sha256=ZDexIIpGwsQ6XsY4Tyo082rJoofdD_JBTV97-mLQUVM,12027
|
|
175
175
|
services/__init__.py,sha256=3Kn4cZweLm7at8wFdBdZ-Zwo8hHcnVIsmY5f29nzi2Y,116
|
|
176
|
-
services/access_guard.py,sha256=
|
|
176
|
+
services/access_guard.py,sha256=fzK6Uagc-pKmMYykXsDKZVQTV78RE8b3zyLAgFAHc1U,77455
|
|
177
177
|
services/access_telemetry.py,sha256=9pOCLS9LgDtM7Z4cCEyl7osxhj-m3wsPubcXolBKuV4,9568
|
|
178
178
|
services/activity_log.py,sha256=PNGeEYlw1Aux-mRU4pKCQdbsSuzLvOg7MYiq-cb4n8g,4473
|
|
179
179
|
services/agent_base.py,sha256=a-gdSd_jtZtbjXo1WS8CnWCagXgKaGZd5ShcG6s0kT4,4809
|
|
180
|
-
services/agent_locks.py,sha256=
|
|
180
|
+
services/agent_locks.py,sha256=nRAPuRSoet7BG9oOsNyA5WhlNy-UMhpsq6z5ggIPmLA,22421
|
|
181
181
|
services/agents.py,sha256=vwfqs8mOlJDXQcGnWOFLPlXnrXraKjiF6lQRsEb0uB4,84469
|
|
182
182
|
services/artifact_defs.py,sha256=M3blGvW3QhKxoVL1bwU7x9DlnXw1_TUHiOahiyp_Wm0,9749
|
|
183
183
|
services/artifact_store.py,sha256=vCHpOzOcZmR5VF_BxcvIh6G0rPMl07c1W6Wdu2OkVVA,31098
|
|
184
|
+
services/atomic_json.py,sha256=ZJ64TGw0ft0o8ypqcmye7Pzs_A2XzJ5jcdahoJGslEs,5299
|
|
184
185
|
services/auto_memory.py,sha256=LAQZtz5qbnn5FSw8hapO9kwbhqLpnQi70IWdNoNQ1V0,15258
|
|
185
186
|
services/background_service.py,sha256=Hn-vjAUq_wPm0cUiwwzyr-D7Xxkygjuy7s3_9XuLaOs,26866
|
|
186
187
|
services/benchmark_dashboard.py,sha256=iR-DnqnoKbqHMJ4d-ZkIvJBYfzwTa7r-jzO6j2BYDfQ,27711
|
|
@@ -261,7 +262,7 @@ services/session_benchmark.py,sha256=KUcWWC5X088p_ao6DgglUMwj7jCzdecPKxJ7XiGKob0
|
|
|
261
262
|
services/session_manager.py,sha256=jX3rQmilylZJrGnxLjghltoi2rs0k5jszIDQ1iTAGpQ,57025
|
|
262
263
|
services/session_preloader.py,sha256=DsTAXMKVtrX9yu1sEFojYDi9-jkSAj1Ylt9JTy57Dow,9883
|
|
263
264
|
services/shell_jobs.py,sha256=p95Sr_lCA04ZkZh3UezeIxxw72_ClDzASaH-T9CV8CA,48581
|
|
264
|
-
services/shell_output.py,sha256=
|
|
265
|
+
services/shell_output.py,sha256=lLvENKqS9N7-hsVw9M7fWUXhBVPaaGRmU9dRx3Vr4Dk,42558
|
|
265
266
|
services/subprojects.py,sha256=pjGbuWLqC81QxYMai9ZbNxmTJTuwzM7QNiyAqeZG-Yg,41218
|
|
266
267
|
services/task_store.py,sha256=QoyhLcAyd-JZxn1nMfnZ11tTLgFqY-xwmlx1q8w_1p8,49477
|
|
267
268
|
services/telemetry.py,sha256=_m7z-RMy_iWQlVYTb52xtJqvvYMbql4MspVvpddOIVg,20176
|
|
@@ -297,8 +298,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
297
298
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
298
299
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
299
300
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
300
|
-
code_context_control-2.118.
|
|
301
|
-
code_context_control-2.118.
|
|
302
|
-
code_context_control-2.118.
|
|
303
|
-
code_context_control-2.118.
|
|
304
|
-
code_context_control-2.118.
|
|
301
|
+
code_context_control-2.118.2.dist-info/METADATA,sha256=J372pWy3pynKt7IYUKIgzgrPcrIASfS2gSo-dFn-enU,26700
|
|
302
|
+
code_context_control-2.118.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
303
|
+
code_context_control-2.118.2.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
304
|
+
code_context_control-2.118.2.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
305
|
+
code_context_control-2.118.2.dist-info/RECORD,,
|
oracle/services/mobile_api.py
CHANGED
|
@@ -56,6 +56,7 @@ from oracle.config import ORACLE_DIR, load_config
|
|
|
56
56
|
from oracle.services import api_auth, discovery_audit
|
|
57
57
|
from oracle.services.api_auth import extract_bearer
|
|
58
58
|
from oracle.services.tool_registry import _c3_version
|
|
59
|
+
from services.atomic_json import write_json_atomic
|
|
59
60
|
|
|
60
61
|
# 2: /info reports EFFECTIVE capabilities (filtered by the config switches)
|
|
61
62
|
# rather than a static list, so a client can gate its UI instead of probing.
|
|
@@ -2090,10 +2091,11 @@ def _write_override_section(project: Path, section: dict) -> dict:
|
|
|
2090
2091
|
else:
|
|
2091
2092
|
block[key] = value
|
|
2092
2093
|
cfg["override"] = block
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2094
|
+
# Shared with the CLI and the hub, and served from a threaded API, so the
|
|
2095
|
+
# publish goes through the one race-safe writer rather than a temp name
|
|
2096
|
+
# every writer of this file would pick. See services.atomic_json.
|
|
2097
|
+
write_json_atomic(cfg_file, cfg, ensure_ascii=False,
|
|
2098
|
+
trailing_newline=False)
|
|
2097
2099
|
return block
|
|
2098
2100
|
|
|
2099
2101
|
|
services/access_guard.py
CHANGED
|
@@ -20,6 +20,8 @@ import re
|
|
|
20
20
|
from dataclasses import dataclass
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
+
from services.atomic_json import write_json_atomic
|
|
24
|
+
|
|
23
25
|
# ── Rule model ──────────────────────────────────────────────────────────────
|
|
24
26
|
|
|
25
27
|
_KIND_DENY = "deny"
|
|
@@ -1473,11 +1475,17 @@ def _load_config_for_write(cfg: Path) -> tuple:
|
|
|
1473
1475
|
|
|
1474
1476
|
|
|
1475
1477
|
def _write_scope_config(cfg: Path, data: dict) -> None:
|
|
1476
|
-
"""Atomic same-directory replace; privileged internal write.
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1478
|
+
"""Atomic same-directory replace; privileged internal write.
|
|
1479
|
+
|
|
1480
|
+
The CLI, the hub and the mobile API all reach this for the same scope
|
|
1481
|
+
file, so the publish is via ``services.atomic_json`` — a shared
|
|
1482
|
+
``config.json.tmp`` is a name they would collide on. The fsync matters
|
|
1483
|
+
more here than anywhere else in the codebase: a truncated ``config.json``
|
|
1484
|
+
comes back from ``load_all`` in its ``corrupt`` list, every path then
|
|
1485
|
+
fails closed with ``<corrupt-config>``, and the session can no longer run
|
|
1486
|
+
the command that would fix it.
|
|
1487
|
+
"""
|
|
1488
|
+
write_json_atomic(cfg, data)
|
|
1481
1489
|
|
|
1482
1490
|
|
|
1483
1491
|
def seed_default_global_rules() -> dict:
|
services/agent_locks.py
CHANGED
|
@@ -41,6 +41,7 @@ import time
|
|
|
41
41
|
import uuid
|
|
42
42
|
from pathlib import Path
|
|
43
43
|
|
|
44
|
+
from services.atomic_json import write_json_atomic
|
|
44
45
|
from services.task_store import _FileLock
|
|
45
46
|
|
|
46
47
|
SCHEMA_VERSION = 1
|
|
@@ -219,10 +220,15 @@ class LockStore:
|
|
|
219
220
|
return doc
|
|
220
221
|
|
|
221
222
|
def _save(self, doc: dict) -> None:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
# The `_FileLock` guard serialises the read-modify-write, but only
|
|
224
|
+
# among callers that take it — and it is advisory, held per process.
|
|
225
|
+
# The publish itself has to be safe on its own: a shared
|
|
226
|
+
# `locks.json.tmp` is one name every writer on the box picks, and two
|
|
227
|
+
# of them landing together either publishes a half-written lease file
|
|
228
|
+
# or raises WinError 5 (see services.atomic_json). Losing this write
|
|
229
|
+
# means losing live leases, which is exactly what `_load` refuses to
|
|
230
|
+
# do when it quarantines a corrupt file instead of resetting it.
|
|
231
|
+
write_json_atomic(self.state_file, doc)
|
|
226
232
|
|
|
227
233
|
def _guard(self):
|
|
228
234
|
return _FileLock(self.lock_file)
|
services/atomic_json.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""One durable same-directory replace for the small JSON stores C3 writes.
|
|
2
|
+
|
|
3
|
+
Why this module exists
|
|
4
|
+
----------------------
|
|
5
|
+
C3 keeps a handful of tiny JSON files that more than one writer touches: the
|
|
6
|
+
agent lock store, the per-scope ``config.json`` written by the CLI, the hub
|
|
7
|
+
and the mobile API, and the shell job store. Every one of them had grown the
|
|
8
|
+
same three-line publish step by copy — ``<name>.tmp`` then a single
|
|
9
|
+
``os.replace`` — and on Windows that shape is a live bug rather than a style
|
|
10
|
+
nit:
|
|
11
|
+
|
|
12
|
+
1. **A shared temp name races on the TEMP, not on the target.**
|
|
13
|
+
``os.replace`` is atomic on the destination; nothing at all protects
|
|
14
|
+
``<name>.tmp``. Two writers of one path both create it, so one publishes
|
|
15
|
+
a file the other is still writing, or the replace raises
|
|
16
|
+
``PermissionError: [WinError 5] Access is denied`` because the loser still
|
|
17
|
+
holds a handle to it. Observed for real in the shell job store, where it
|
|
18
|
+
crashed a supervisor and reported "failed before running" for a command
|
|
19
|
+
that was fine (fixed there in 2.118.1, PR #157). The other copies had the
|
|
20
|
+
identical shape and no demonstrated failure only because they are written
|
|
21
|
+
far less often — the lock store especially is a coin that had not landed
|
|
22
|
+
on its edge yet.
|
|
23
|
+
2. **A pid alone is not a unique suffix.** Threads share a pid, and the hub,
|
|
24
|
+
the Oracle and the lock store are all written from threaded servers. Six
|
|
25
|
+
threads are enough to drive a ``.tmp<pid>`` implementation into both
|
|
26
|
+
``PermissionError`` and ``FileNotFoundError`` within a second, so the
|
|
27
|
+
suffix carries per-WRITE randomness as well.
|
|
28
|
+
3. **One attempt turns a transient sharing violation into a lost write.** An
|
|
29
|
+
AV scanner or the Search indexer holding the target open for a few
|
|
30
|
+
milliseconds is ordinary on Windows and clears on its own; a single
|
|
31
|
+
``os.replace`` turns it into an exception at the call site.
|
|
32
|
+
|
|
33
|
+
``cli/_hook_utils._atomic_write_json`` deliberately does NOT import this. A
|
|
34
|
+
PreToolUse hook is a separate, short-lived, single-threaded process that
|
|
35
|
+
imports nothing from ``services`` on purpose, so pid-only is genuinely
|
|
36
|
+
enough there. That one duplication stays.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import secrets
|
|
44
|
+
import time
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
# Four attempts across ~0.14s of backoff. Sized for a scanner or indexer
|
|
48
|
+
# holding a handle, not for a lock a peer holds for seconds — a failure that
|
|
49
|
+
# survives this is real and is re-raised.
|
|
50
|
+
REPLACE_ATTEMPTS = 4
|
|
51
|
+
REPLACE_BACKOFF_S = 0.02
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def atomic_tmp_path(path: Path) -> Path:
|
|
55
|
+
"""``<name>.tmp<pid>-<random>`` beside ``path`` — unique per write."""
|
|
56
|
+
return path.with_name(f"{path.name}.tmp{os.getpid()}-{secrets.token_hex(4)}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_text_atomic(path, text: str, *, encoding: str = "utf-8",
|
|
60
|
+
fsync: bool = True) -> None:
|
|
61
|
+
"""Publish ``text`` at ``path``: unique temp, then a retried ``os.replace``.
|
|
62
|
+
|
|
63
|
+
``fsync`` flushes the temp's bytes before the replace, so a crash or power
|
|
64
|
+
loss between the two cannot publish a zero-length or half-written file.
|
|
65
|
+
Durable state wants that: a truncated ``config.json`` makes the Access
|
|
66
|
+
Guard read ``<corrupt-config>`` and fail closed on every subsequent call,
|
|
67
|
+
which wedges the whole session. Callers whose file is ephemeral spill
|
|
68
|
+
state pass ``fsync=False`` rather than pay a disk flush per update.
|
|
69
|
+
|
|
70
|
+
Newline translation matches ``Path.write_text`` (the default), so this is
|
|
71
|
+
a drop-in for the ``write_text`` + ``os.replace`` pairs it replaces and
|
|
72
|
+
does not churn line endings in files already on disk.
|
|
73
|
+
"""
|
|
74
|
+
path = Path(path)
|
|
75
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
tmp = atomic_tmp_path(path)
|
|
77
|
+
try:
|
|
78
|
+
with open(tmp, "w", encoding=encoding) as fh:
|
|
79
|
+
fh.write(text)
|
|
80
|
+
if fsync:
|
|
81
|
+
fh.flush()
|
|
82
|
+
os.fsync(fh.fileno())
|
|
83
|
+
last_exc: OSError | None = None
|
|
84
|
+
for attempt in range(REPLACE_ATTEMPTS):
|
|
85
|
+
try:
|
|
86
|
+
os.replace(tmp, path)
|
|
87
|
+
return
|
|
88
|
+
except PermissionError as exc:
|
|
89
|
+
last_exc = exc
|
|
90
|
+
if attempt < REPLACE_ATTEMPTS - 1:
|
|
91
|
+
time.sleep(REPLACE_BACKOFF_S * (2 ** attempt))
|
|
92
|
+
if last_exc is not None:
|
|
93
|
+
raise last_exc
|
|
94
|
+
finally:
|
|
95
|
+
# A successful replace already consumed tmp, so this only fires on the
|
|
96
|
+
# failure paths — otherwise abandoned temps accumulate next to the
|
|
97
|
+
# file, and for `.c3/` that means they get indexed and committed.
|
|
98
|
+
try:
|
|
99
|
+
if tmp.exists():
|
|
100
|
+
tmp.unlink()
|
|
101
|
+
except OSError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def write_json_atomic(path, data, *, indent: int = 2,
|
|
106
|
+
ensure_ascii: bool = True,
|
|
107
|
+
trailing_newline: bool = True,
|
|
108
|
+
fsync: bool = True) -> None:
|
|
109
|
+
"""``write_text_atomic`` over ``json.dumps``.
|
|
110
|
+
|
|
111
|
+
The serialisation knobs exist so each caller keeps the exact bytes it
|
|
112
|
+
wrote before this module: the point of the change is the publish step,
|
|
113
|
+
not a reformat of five JSON files that other tools and diffs already read.
|
|
114
|
+
"""
|
|
115
|
+
text = json.dumps(data, indent=indent, ensure_ascii=ensure_ascii)
|
|
116
|
+
if trailing_newline:
|
|
117
|
+
text += "\n"
|
|
118
|
+
write_text_atomic(path, text, fsync=fsync)
|
services/shell_output.py
CHANGED
|
@@ -75,6 +75,8 @@ from dataclasses import dataclass, field
|
|
|
75
75
|
from datetime import datetime, timedelta, timezone
|
|
76
76
|
from pathlib import Path
|
|
77
77
|
|
|
78
|
+
from services.atomic_json import write_json_atomic
|
|
79
|
+
|
|
78
80
|
__all__ = [
|
|
79
81
|
"STORE_ROOT", "RETENTION_DAYS", "RETENTION_BYTES", "HEAD_BYTES", "TAIL_BYTES",
|
|
80
82
|
"TEXT_MAX_BYTES", "DEFAULT_READ_BYTES", "LINE_CLIP_CHARS",
|
|
@@ -961,9 +963,21 @@ def _load_meta(path: Path) -> OutputMeta | None:
|
|
|
961
963
|
|
|
962
964
|
|
|
963
965
|
def _write_json_atomic(path: Path, data: dict) -> None:
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
os.replace
|
|
966
|
+
"""Publish a job record durably — see ``services.atomic_json``.
|
|
967
|
+
|
|
968
|
+
This was the first copy of the unique-temp + retried-``os.replace`` shape
|
|
969
|
+
(2.118.1, PR #157, after a shared ``<name>.tmp`` crashed supervisors on
|
|
970
|
+
Windows CI with WinError 5 and reported "failed before running" for
|
|
971
|
+
commands that were fine). The implementation now lives in one place so the
|
|
972
|
+
lock store and the three ``config.json`` writers share it instead of
|
|
973
|
+
carrying four more copies of a bug.
|
|
974
|
+
|
|
975
|
+
``fsync=False`` is the one thing kept local: a job record is ephemeral
|
|
976
|
+
spill state that a crash invalidates anyway, so a disk flush per status
|
|
977
|
+
update buys nothing. The durable stores take the default.
|
|
978
|
+
"""
|
|
979
|
+
write_json_atomic(path, data, indent=1, ensure_ascii=False,
|
|
980
|
+
trailing_newline=False, fsync=False)
|
|
967
981
|
|
|
968
982
|
|
|
969
983
|
def _move(src: Path, dst: Path) -> None:
|
|
File without changes
|
{code_context_control-2.118.0.dist-info → code_context_control-2.118.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{code_context_control-2.118.0.dist-info → code_context_control-2.118.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{code_context_control-2.118.0.dist-info → code_context_control-2.118.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|