cdx-manager 0.9.13 → 0.9.15
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.
- package/README.md +4 -4
- package/bin/python-runner.js +4 -7
- package/changelogs/CHANGELOGS_0_9_14.md +27 -0
- package/changelogs/CHANGELOGS_0_9_15.md +39 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/backup_bundle.py +2 -0
- package/src/claude_refresh.py +9 -2
- package/src/claude_usage.py +36 -5
- package/src/cli.py +7 -6
- package/src/cli_args.py +23 -7
- package/src/cli_commands.py +37 -23
- package/src/cli_render.py +1 -1
- package/src/codex_usage.py +3 -1
- package/src/context_store.py +3 -4
- package/src/fs_utils.py +32 -0
- package/src/provider_runtime.py +45 -13
- package/src/run_registry.py +47 -14
- package/src/run_usage.py +3 -1
- package/src/session_service.py +137 -100
- package/src/session_store.py +11 -0
- package/src/status_source.py +101 -25
- package/src/status_view.py +19 -6
- package/src/update_check.py +4 -0
- package/src/update_manager.py +3 -1
package/src/provider_runtime.py
CHANGED
|
@@ -695,6 +695,45 @@ def _redact_sensitive_args(spec):
|
|
|
695
695
|
return [REDACTED_PROMPT_ARG if arg in sensitive else arg for arg in args]
|
|
696
696
|
|
|
697
697
|
|
|
698
|
+
def _signal_child_group(child, sig):
|
|
699
|
+
# Providers spawn helpers of their own; signal the whole group so
|
|
700
|
+
# grandchildren die with the child. Falls back to False when the child
|
|
701
|
+
# shares our group (test doubles spawned without start_new_session).
|
|
702
|
+
pid = getattr(child, "pid", None)
|
|
703
|
+
if not pid or not hasattr(os, "getpgid"):
|
|
704
|
+
return False
|
|
705
|
+
try:
|
|
706
|
+
pgid = os.getpgid(pid)
|
|
707
|
+
if pgid == os.getpgid(0):
|
|
708
|
+
return False
|
|
709
|
+
os.killpg(pgid, sig)
|
|
710
|
+
return True
|
|
711
|
+
except (OSError, TypeError):
|
|
712
|
+
return False
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _terminate_child_tree(child):
|
|
716
|
+
if not _signal_child_group(child, signal.SIGTERM):
|
|
717
|
+
try:
|
|
718
|
+
child.terminate()
|
|
719
|
+
except Exception:
|
|
720
|
+
pass
|
|
721
|
+
try:
|
|
722
|
+
child.wait(timeout=5)
|
|
723
|
+
return
|
|
724
|
+
except Exception:
|
|
725
|
+
pass
|
|
726
|
+
if not _signal_child_group(child, signal.SIGKILL):
|
|
727
|
+
try:
|
|
728
|
+
child.kill()
|
|
729
|
+
except Exception:
|
|
730
|
+
pass
|
|
731
|
+
try:
|
|
732
|
+
child.wait()
|
|
733
|
+
except Exception:
|
|
734
|
+
pass
|
|
735
|
+
|
|
736
|
+
|
|
698
737
|
def _run_headless_provider_command(session, cwd=None, env_override=None, initial_prompt=None,
|
|
699
738
|
timeout_seconds=None, spawn=None, run_id=None):
|
|
700
739
|
spawn = spawn or subprocess.Popen
|
|
@@ -714,12 +753,16 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
|
|
|
714
753
|
timed_out = False
|
|
715
754
|
with open(paths["stdout_path"], "w", encoding="utf-8", errors="replace") as stdout_file, \
|
|
716
755
|
open(paths["stderr_path"], "w", encoding="utf-8", errors="replace") as stderr_file:
|
|
756
|
+
options = {k: v for k, v in spec.get("options", {}).items() if k not in ("stdio", "stdout", "stderr")}
|
|
757
|
+
if spawn is subprocess.Popen:
|
|
758
|
+
# Own process group so a timeout can kill grandchildren too.
|
|
759
|
+
options["start_new_session"] = True
|
|
717
760
|
try:
|
|
718
761
|
child = spawn(
|
|
719
762
|
[command] + spec["args"],
|
|
720
763
|
stdout=stdout_file,
|
|
721
764
|
stderr=stderr_file,
|
|
722
|
-
**
|
|
765
|
+
**options,
|
|
723
766
|
)
|
|
724
767
|
except FileNotFoundError as error:
|
|
725
768
|
_combine_headless_transcript(paths)
|
|
@@ -740,18 +783,7 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
|
|
|
740
783
|
child.wait()
|
|
741
784
|
except subprocess.TimeoutExpired:
|
|
742
785
|
timed_out = True
|
|
743
|
-
|
|
744
|
-
child.terminate()
|
|
745
|
-
child.wait(timeout=5)
|
|
746
|
-
except Exception:
|
|
747
|
-
try:
|
|
748
|
-
child.kill()
|
|
749
|
-
except Exception:
|
|
750
|
-
pass
|
|
751
|
-
try:
|
|
752
|
-
child.wait()
|
|
753
|
-
except Exception:
|
|
754
|
-
pass
|
|
786
|
+
_terminate_child_tree(child)
|
|
755
787
|
|
|
756
788
|
_combine_headless_transcript(paths)
|
|
757
789
|
end_time = datetime.now(timezone.utc)
|
package/src/run_registry.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
import sys
|
|
3
4
|
import tempfile
|
|
5
|
+
from contextlib import contextmanager
|
|
4
6
|
from datetime import datetime, timezone
|
|
5
7
|
|
|
6
8
|
|
|
@@ -12,6 +14,28 @@ def registry_path(base_dir):
|
|
|
12
14
|
return os.path.join(base_dir, "runs.json")
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
@contextmanager
|
|
18
|
+
def _registry_lock(path):
|
|
19
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
20
|
+
with open(path + ".lock", "a") as handle:
|
|
21
|
+
if sys.platform == "win32":
|
|
22
|
+
import msvcrt
|
|
23
|
+
handle.seek(0)
|
|
24
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
25
|
+
try:
|
|
26
|
+
yield
|
|
27
|
+
finally:
|
|
28
|
+
handle.seek(0)
|
|
29
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
30
|
+
else:
|
|
31
|
+
import fcntl
|
|
32
|
+
fcntl.flock(handle, fcntl.LOCK_EX)
|
|
33
|
+
try:
|
|
34
|
+
yield
|
|
35
|
+
finally:
|
|
36
|
+
fcntl.flock(handle, fcntl.LOCK_UN)
|
|
37
|
+
|
|
38
|
+
|
|
15
39
|
def _read_registry(path):
|
|
16
40
|
try:
|
|
17
41
|
with open(path, encoding="utf-8") as handle:
|
|
@@ -76,7 +100,9 @@ def _base_record(run_id, *, kind, session, provider, model, cwd, artifacts=None)
|
|
|
76
100
|
"provider": provider,
|
|
77
101
|
"model": model,
|
|
78
102
|
"cwd": os.path.abspath(cwd),
|
|
79
|
-
|
|
103
|
+
# The manager process drives the run synchronously, so its own pid is
|
|
104
|
+
# the liveness proxy until finish() records the provider child's pid.
|
|
105
|
+
"pid": os.getpid(),
|
|
80
106
|
"started_at": utc_now_iso(),
|
|
81
107
|
"ended_at": None,
|
|
82
108
|
"duration_seconds": None,
|
|
@@ -94,14 +120,19 @@ class RunRegistry:
|
|
|
94
120
|
self.path = registry_path(base_dir)
|
|
95
121
|
|
|
96
122
|
def start(self, run_id, *, kind, session, provider, model, cwd, artifacts=None):
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
with _registry_lock(self.path):
|
|
124
|
+
data = _read_registry(self.path)
|
|
125
|
+
data["runs"] = [run for run in data["runs"] if run.get("run_id") != run_id]
|
|
126
|
+
record = _base_record(run_id, kind=kind, session=session, provider=provider, model=model, cwd=cwd, artifacts=artifacts)
|
|
127
|
+
data["runs"].insert(0, record)
|
|
128
|
+
_write_registry(self.path, data)
|
|
102
129
|
return record
|
|
103
130
|
|
|
104
131
|
def finish(self, run_id, *, status, final_payload=None, run_info=None, error=None, task_report=None):
|
|
132
|
+
with _registry_lock(self.path):
|
|
133
|
+
return self._finish_locked(run_id, status=status, final_payload=final_payload, run_info=run_info, error=error, task_report=task_report)
|
|
134
|
+
|
|
135
|
+
def _finish_locked(self, run_id, *, status, final_payload=None, run_info=None, error=None, task_report=None):
|
|
105
136
|
data = _read_registry(self.path)
|
|
106
137
|
now = utc_now_iso()
|
|
107
138
|
for run in data["runs"]:
|
|
@@ -137,17 +168,19 @@ class RunRegistry:
|
|
|
137
168
|
return None
|
|
138
169
|
|
|
139
170
|
def list(self, limit=20):
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
171
|
+
with _registry_lock(self.path):
|
|
172
|
+
data = _read_registry(self.path)
|
|
173
|
+
changed = _refresh_stale_runs(data)
|
|
174
|
+
if changed:
|
|
175
|
+
_write_registry(self.path, data)
|
|
144
176
|
return data["runs"][: max(0, int(limit or 20))]
|
|
145
177
|
|
|
146
178
|
def get(self, run_id):
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
179
|
+
with _registry_lock(self.path):
|
|
180
|
+
data = _read_registry(self.path)
|
|
181
|
+
changed = _refresh_stale_runs(data)
|
|
182
|
+
if changed:
|
|
183
|
+
_write_registry(self.path, data)
|
|
151
184
|
for run in data["runs"]:
|
|
152
185
|
if run.get("run_id") == run_id:
|
|
153
186
|
return run
|
package/src/run_usage.py
CHANGED
|
@@ -46,7 +46,9 @@ def _parse_json_records(text):
|
|
|
46
46
|
try:
|
|
47
47
|
records.append(json.loads(line))
|
|
48
48
|
except json.JSONDecodeError:
|
|
49
|
-
|
|
49
|
+
# A single truncated/noisy line must not discard the usage
|
|
50
|
+
# carried by every other valid JSONL record in the stream.
|
|
51
|
+
continue
|
|
50
52
|
return records
|
|
51
53
|
|
|
52
54
|
|
package/src/session_service.py
CHANGED
|
@@ -14,7 +14,7 @@ from .claude_usage import _decode_jwt_claims
|
|
|
14
14
|
from .codex_usage import fetch_codex_rate_limits
|
|
15
15
|
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDERS, get_cdx_home
|
|
16
16
|
from .errors import CdxError
|
|
17
|
-
from .fs_utils import remove_tree
|
|
17
|
+
from .fs_utils import atomic_write, remove_tree
|
|
18
18
|
from .session_store import create_session_store
|
|
19
19
|
from .status_source import find_latest_status_artifact
|
|
20
20
|
|
|
@@ -266,6 +266,7 @@ def _normalize_status_payload(payload=None):
|
|
|
266
266
|
"updated_at": _to_local_iso(payload.get("updated_at") or payload.get("captured_at") or now),
|
|
267
267
|
"raw_status_text": payload.get("raw_status_text"),
|
|
268
268
|
"source_ref": payload.get("source_ref"),
|
|
269
|
+
"structured": bool(payload.get("structured")),
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
|
|
@@ -366,6 +367,12 @@ def _merge_status_payload(current, candidate):
|
|
|
366
367
|
if merged.get(field) is None and candidate.get(field) is not None:
|
|
367
368
|
merged[field] = candidate[field]
|
|
368
369
|
|
|
370
|
+
# The structured marker qualifies the source_ref; keep them in sync when
|
|
371
|
+
# the merge adopts the candidate's ref, else a structured rollout ref
|
|
372
|
+
# would read as low-confidence scraped text.
|
|
373
|
+
if merged.get("source_ref") is not None and merged["source_ref"] == candidate.get("source_ref"):
|
|
374
|
+
merged["structured"] = bool(candidate.get("structured"))
|
|
375
|
+
|
|
369
376
|
merged["updated_at"] = candidate.get("updated_at") or current.get("updated_at")
|
|
370
377
|
return merged
|
|
371
378
|
|
|
@@ -386,6 +393,10 @@ def _compute_available_pct(status):
|
|
|
386
393
|
def _is_low_confidence_status_source(status):
|
|
387
394
|
if not status:
|
|
388
395
|
return False
|
|
396
|
+
if status.get("structured"):
|
|
397
|
+
# Exact rate_limits API data: trustworthy even when it comes from a
|
|
398
|
+
# sessions/rollout path, which otherwise flags scraped-text noise.
|
|
399
|
+
return False
|
|
389
400
|
source_ref = str(status.get("source_ref") or "").replace(os.sep, "/")
|
|
390
401
|
return "/sessions/" in source_ref and "/rollout" in source_ref
|
|
391
402
|
|
|
@@ -425,9 +436,7 @@ def _ensure_claude_attribution_disabled(auth_home):
|
|
|
425
436
|
settings = {}
|
|
426
437
|
settings["includeCoAuthoredBy"] = False
|
|
427
438
|
try:
|
|
428
|
-
|
|
429
|
-
json.dump(settings, handle, indent=2, sort_keys=True)
|
|
430
|
-
handle.write("\n")
|
|
439
|
+
atomic_write(settings_path, json.dumps(settings, indent=2, sort_keys=True) + "\n", mode=0o644)
|
|
431
440
|
except OSError:
|
|
432
441
|
return False
|
|
433
442
|
return True
|
|
@@ -478,23 +487,29 @@ def create_session_service(options=None):
|
|
|
478
487
|
)
|
|
479
488
|
|
|
480
489
|
def _session_runtime(name):
|
|
481
|
-
|
|
482
|
-
|
|
490
|
+
found = {}
|
|
491
|
+
|
|
492
|
+
def updater(state):
|
|
493
|
+
if not state:
|
|
494
|
+
return None
|
|
495
|
+
runtime = state.get("runtime")
|
|
496
|
+
if _runtime_is_active(runtime):
|
|
497
|
+
found["runtime"] = runtime
|
|
498
|
+
return None
|
|
499
|
+
if isinstance(runtime, dict) and runtime.get("status") == "running":
|
|
500
|
+
return {
|
|
501
|
+
**state,
|
|
502
|
+
"status": "ready",
|
|
503
|
+
"runtime": {
|
|
504
|
+
**runtime,
|
|
505
|
+
"status": "stale",
|
|
506
|
+
"endedAt": _local_now_iso(),
|
|
507
|
+
},
|
|
508
|
+
}
|
|
483
509
|
return None
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if isinstance(runtime, dict) and runtime.get("status") == "running":
|
|
488
|
-
store["write_session_state"](name, {
|
|
489
|
-
**state,
|
|
490
|
-
"status": "ready",
|
|
491
|
-
"runtime": {
|
|
492
|
-
**runtime,
|
|
493
|
-
"status": "stale",
|
|
494
|
-
"endedAt": _local_now_iso(),
|
|
495
|
-
},
|
|
496
|
-
})
|
|
497
|
-
return None
|
|
510
|
+
|
|
511
|
+
store["update_session_state"](name, updater)
|
|
512
|
+
return found.get("runtime")
|
|
498
513
|
|
|
499
514
|
def _validate_new_session_name(name):
|
|
500
515
|
if not name:
|
|
@@ -755,11 +770,14 @@ def create_session_service(options=None):
|
|
|
755
770
|
raise CdxError(f"Unknown session: {name}")
|
|
756
771
|
if session.get("enabled", True) is False:
|
|
757
772
|
raise CdxError(f"Session is disabled: {name}")
|
|
758
|
-
state = store["read_session_state"](name)
|
|
759
|
-
if not state:
|
|
760
|
-
raise CdxError(f"Session state missing for {name}. Reconnect required.")
|
|
761
773
|
now = _local_now_iso()
|
|
762
|
-
|
|
774
|
+
|
|
775
|
+
def state_updater(state):
|
|
776
|
+
if not state:
|
|
777
|
+
raise CdxError(f"Session state missing for {name}. Reconnect required.")
|
|
778
|
+
return {**state, "rehydratedAt": now}
|
|
779
|
+
|
|
780
|
+
store["update_session_state"](name, state_updater)
|
|
763
781
|
return store["update_session"](name, lambda s: {
|
|
764
782
|
**s, "updatedAt": now, "lastLaunchedAt": now
|
|
765
783
|
})
|
|
@@ -768,9 +786,6 @@ def create_session_service(options=None):
|
|
|
768
786
|
session = store["get_session"](name)
|
|
769
787
|
if not session:
|
|
770
788
|
raise CdxError(f"Unknown session: {name}")
|
|
771
|
-
state = store["read_session_state"](name)
|
|
772
|
-
if not state:
|
|
773
|
-
raise CdxError(f"Session state missing for {name}. Reconnect required.")
|
|
774
789
|
payload = dict(payload or {})
|
|
775
790
|
now = _local_now_iso()
|
|
776
791
|
runtime = {
|
|
@@ -782,52 +797,70 @@ def create_session_service(options=None):
|
|
|
782
797
|
"label": payload.get("label"),
|
|
783
798
|
"transcriptPath": payload.get("transcript_path") or payload.get("transcriptPath"),
|
|
784
799
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
800
|
+
|
|
801
|
+
def updater(state):
|
|
802
|
+
if not state:
|
|
803
|
+
raise CdxError(f"Session state missing for {name}. Reconnect required.")
|
|
804
|
+
return {
|
|
805
|
+
**state,
|
|
806
|
+
"status": "running",
|
|
807
|
+
"runtime": runtime,
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
store["update_session_state"](name, updater)
|
|
790
811
|
return runtime
|
|
791
812
|
|
|
792
813
|
def finish_session_runtime(name, run_id=None, payload=None):
|
|
793
814
|
session = store["get_session"](name)
|
|
794
815
|
if not session:
|
|
795
816
|
raise CdxError(f"Unknown session: {name}")
|
|
796
|
-
state = store["read_session_state"](name)
|
|
797
|
-
if not state:
|
|
798
|
-
return None
|
|
799
|
-
runtime = state.get("runtime") or {}
|
|
800
|
-
if run_id and runtime.get("runId") != run_id:
|
|
801
|
-
return runtime
|
|
802
817
|
payload = dict(payload or {})
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
818
|
+
outcome = {}
|
|
819
|
+
|
|
820
|
+
def updater(state):
|
|
821
|
+
if not state:
|
|
822
|
+
return None
|
|
823
|
+
runtime = state.get("runtime") or {}
|
|
824
|
+
if run_id and runtime.get("runId") != run_id:
|
|
825
|
+
outcome["runtime"] = runtime
|
|
826
|
+
return None
|
|
827
|
+
updated_runtime = {
|
|
828
|
+
**runtime,
|
|
829
|
+
"status": payload.get("status") or "stopped",
|
|
830
|
+
"endedAt": payload.get("endedAt") or _local_now_iso(),
|
|
831
|
+
}
|
|
832
|
+
if "returncode" in payload:
|
|
833
|
+
updated_runtime["returncode"] = payload["returncode"]
|
|
834
|
+
outcome["runtime"] = updated_runtime
|
|
835
|
+
return {
|
|
836
|
+
**state,
|
|
837
|
+
"status": "ready",
|
|
838
|
+
"runtime": updated_runtime,
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
store["update_session_state"](name, updater)
|
|
842
|
+
return outcome.get("runtime")
|
|
816
843
|
|
|
817
844
|
def ensure_session_state(name):
|
|
818
845
|
session = store["get_session"](name)
|
|
819
846
|
if not session:
|
|
820
847
|
raise CdxError(f"Unknown session: {name}")
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
848
|
+
result = {}
|
|
849
|
+
|
|
850
|
+
def updater(state):
|
|
851
|
+
if state:
|
|
852
|
+
result["state"] = state
|
|
853
|
+
return None
|
|
854
|
+
repaired = {
|
|
855
|
+
"provider": session["provider"],
|
|
856
|
+
"status": "ready",
|
|
857
|
+
"rehydratedAt": None,
|
|
858
|
+
}
|
|
859
|
+
result["state"] = repaired
|
|
860
|
+
return repaired
|
|
861
|
+
|
|
862
|
+
store["update_session_state"](name, updater)
|
|
863
|
+
return result["state"]
|
|
831
864
|
|
|
832
865
|
def list_sessions():
|
|
833
866
|
return store["list_sessions"]()
|
|
@@ -853,22 +886,28 @@ def create_session_service(options=None):
|
|
|
853
886
|
updates = _normalize_launch_settings(settings)
|
|
854
887
|
if not updates:
|
|
855
888
|
raise CdxError("At least one launch setting is required.")
|
|
856
|
-
current = _normalize_launch_settings(session.get("launch") or {}, mark_fast_service_tier=False)
|
|
857
|
-
launch = {**current, **updates}
|
|
858
|
-
explicit_power = "power" in updates or "reasoning_effort" in updates
|
|
859
|
-
if explicit_power and "fast" not in updates and launch.get("fastMode") != "service_tier":
|
|
860
|
-
launch["fast"] = False
|
|
861
|
-
launch.pop("fastMode", None)
|
|
862
|
-
if updates.get("fast") is False:
|
|
863
|
-
launch.pop("fastMode", None)
|
|
864
|
-
if not any(key in launch for key in ("power", "reasoning_effort", "reasoningEffort")):
|
|
865
|
-
launch["power"] = DEFAULT_LAUNCH_SETTINGS["power"]
|
|
866
889
|
now = _local_now_iso()
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
890
|
+
|
|
891
|
+
def updater(s):
|
|
892
|
+
# Build the new launch dict from the fresh record inside the store
|
|
893
|
+
# lock; a pre-lock snapshot loses a concurrent setting change.
|
|
894
|
+
current = _normalize_launch_settings(s.get("launch") or {}, mark_fast_service_tier=False)
|
|
895
|
+
launch = {**current, **updates}
|
|
896
|
+
explicit_power = "power" in updates or "reasoning_effort" in updates
|
|
897
|
+
if explicit_power and "fast" not in updates and launch.get("fastMode") != "service_tier":
|
|
898
|
+
launch["fast"] = False
|
|
899
|
+
launch.pop("fastMode", None)
|
|
900
|
+
if updates.get("fast") is False:
|
|
901
|
+
launch.pop("fastMode", None)
|
|
902
|
+
if not any(key in launch for key in ("power", "reasoning_effort", "reasoningEffort")):
|
|
903
|
+
launch["power"] = DEFAULT_LAUNCH_SETTINGS["power"]
|
|
904
|
+
return {
|
|
905
|
+
**s,
|
|
906
|
+
"launch": launch,
|
|
907
|
+
"updatedAt": now,
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return store["update_session"](name, updater)
|
|
872
911
|
|
|
873
912
|
def unset_launch_settings(name, keys):
|
|
874
913
|
session = store["get_session"](name)
|
|
@@ -880,12 +919,12 @@ def create_session_service(options=None):
|
|
|
880
919
|
unknown = [key for key in keys if key not in allowed]
|
|
881
920
|
if unknown:
|
|
882
921
|
raise CdxError(f"Unsupported launch setting: {', '.join(unknown)}")
|
|
883
|
-
current = dict(session.get("launch") or {})
|
|
884
|
-
for key in keys:
|
|
885
|
-
current.pop(key, None)
|
|
886
922
|
now = _local_now_iso()
|
|
887
923
|
|
|
888
924
|
def updater(s):
|
|
925
|
+
current = dict(s.get("launch") or {})
|
|
926
|
+
for key in keys:
|
|
927
|
+
current.pop(key, None)
|
|
889
928
|
updated = {**s, "updatedAt": now}
|
|
890
929
|
if current:
|
|
891
930
|
updated["launch"] = current
|
|
@@ -972,6 +1011,9 @@ def create_session_service(options=None):
|
|
|
972
1011
|
global_root,
|
|
973
1012
|
session["provider"],
|
|
974
1013
|
expected_account_email=expected_account_email,
|
|
1014
|
+
# The shared codex home can hold other accounts' rollouts;
|
|
1015
|
+
# structured payloads there cannot be attributed to ours.
|
|
1016
|
+
trust_unattributed_structured=False,
|
|
975
1017
|
)
|
|
976
1018
|
if not artifact:
|
|
977
1019
|
if _is_low_confidence_status_source(current_status):
|
|
@@ -988,6 +1030,7 @@ def create_session_service(options=None):
|
|
|
988
1030
|
"updated_at": artifact.get("updated_at"),
|
|
989
1031
|
"raw_status_text": artifact.get("raw_status_text"),
|
|
990
1032
|
"source_ref": artifact.get("source_ref"),
|
|
1033
|
+
"structured": artifact.get("structured"),
|
|
991
1034
|
})
|
|
992
1035
|
if _is_low_confidence_status_source(current_status) and not _is_low_confidence_status_source(resolved):
|
|
993
1036
|
record_status(session["name"], resolved)
|
|
@@ -1269,14 +1312,10 @@ def create_session_service(options=None):
|
|
|
1269
1312
|
bundle_bytes = encode_bundle(payload, include_auth=include_auth, passphrase=passphrase)
|
|
1270
1313
|
if progress_callback:
|
|
1271
1314
|
progress_callback({"event": "writing_started", "path": file_path, "bundle_size_bytes": len(bundle_bytes)})
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
try:
|
|
1277
|
-
os.chmod(file_path, 0o600)
|
|
1278
|
-
except OSError:
|
|
1279
|
-
pass
|
|
1315
|
+
# Atomic + 0o600 from the start: the export path may live in a
|
|
1316
|
+
# world-traversable directory, and a crash must not clobber a
|
|
1317
|
+
# previous good bundle with a truncated one.
|
|
1318
|
+
atomic_write(os.path.abspath(file_path), bundle_bytes, mode=0o600)
|
|
1280
1319
|
return {
|
|
1281
1320
|
"path": file_path,
|
|
1282
1321
|
"include_auth": include_auth,
|
|
@@ -1327,8 +1366,12 @@ def create_session_service(options=None):
|
|
|
1327
1366
|
_ensure_private_dir(session_root)
|
|
1328
1367
|
_ensure_private_dir(auth_home)
|
|
1329
1368
|
|
|
1369
|
+
existing_state_before = None
|
|
1330
1370
|
if is_existing and merge:
|
|
1331
1371
|
existing_record = store["get_session"](name) or {}
|
|
1372
|
+
# replace_session resets the state file to defaults, so the
|
|
1373
|
+
# local state must be captured before it runs.
|
|
1374
|
+
existing_state_before = store["read_session_state"](name) or {}
|
|
1332
1375
|
bundle_record = {
|
|
1333
1376
|
**session_payload,
|
|
1334
1377
|
"provider": provider,
|
|
@@ -1352,13 +1395,12 @@ def create_session_service(options=None):
|
|
|
1352
1395
|
store["replace_session"](name, session_record)
|
|
1353
1396
|
|
|
1354
1397
|
state = (payload.get("states") or {}).get(name)
|
|
1355
|
-
if
|
|
1356
|
-
if
|
|
1357
|
-
|
|
1358
|
-
merged_state = {**state, **{k: v for k, v in existing_state.items() if v is not None}}
|
|
1398
|
+
if is_existing and merge:
|
|
1399
|
+
merged_state = {**(state or {}), **{k: v for k, v in (existing_state_before or {}).items() if v is not None}}
|
|
1400
|
+
if merged_state:
|
|
1359
1401
|
store["write_session_state"](name, merged_state)
|
|
1360
|
-
|
|
1361
|
-
|
|
1402
|
+
elif state is not None:
|
|
1403
|
+
store["write_session_state"](name, state)
|
|
1362
1404
|
|
|
1363
1405
|
for item in (payload.get("profiles") or {}).get(name, []):
|
|
1364
1406
|
rel_path = _safe_relpath(item.get("path"))
|
|
@@ -1371,13 +1413,8 @@ def create_session_service(options=None):
|
|
|
1371
1413
|
if is_existing and merge and os.path.exists(dest_path):
|
|
1372
1414
|
continue
|
|
1373
1415
|
_ensure_private_dir(os.path.dirname(dest_path))
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
if sys.platform != "win32":
|
|
1377
|
-
try:
|
|
1378
|
-
os.chmod(dest_path, 0o600)
|
|
1379
|
-
except OSError:
|
|
1380
|
-
pass
|
|
1416
|
+
# Decrypted credentials: 0o600 from creation, no umask window.
|
|
1417
|
+
atomic_write(dest_path, content, mode=0o600)
|
|
1381
1418
|
|
|
1382
1419
|
return {
|
|
1383
1420
|
"path": file_path,
|
package/src/session_store.py
CHANGED
|
@@ -260,6 +260,16 @@ def create_session_store(base_dir):
|
|
|
260
260
|
with _file_lock(lock_file):
|
|
261
261
|
_write_session_state_unlocked(name, state)
|
|
262
262
|
|
|
263
|
+
def update_session_state(name, updater):
|
|
264
|
+
# Read-modify-write under one lock; separate read/write calls let a
|
|
265
|
+
# concurrent writer's update be clobbered between the two.
|
|
266
|
+
with _file_lock(lock_file):
|
|
267
|
+
state = _read_session_state_unlocked(name)
|
|
268
|
+
updated = updater(state)
|
|
269
|
+
if updated is not None:
|
|
270
|
+
_write_session_state_unlocked(name, updated)
|
|
271
|
+
return updated
|
|
272
|
+
|
|
263
273
|
def append_launch_history(entry):
|
|
264
274
|
with _file_lock(lock_file):
|
|
265
275
|
_ensure_dir(state_dir)
|
|
@@ -303,6 +313,7 @@ def create_session_store(base_dir):
|
|
|
303
313
|
"replace_session": replace_session,
|
|
304
314
|
"read_session_state": read_session_state,
|
|
305
315
|
"write_session_state": write_session_state,
|
|
316
|
+
"update_session_state": update_session_state,
|
|
306
317
|
"append_launch_history": append_launch_history,
|
|
307
318
|
"list_launch_history": list_launch_history,
|
|
308
319
|
}
|