cctally 1.77.0 → 1.79.0
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/CHANGELOG.md +28 -0
- package/README.md +2 -0
- package/bin/_cctally_cache.py +140 -2
- package/bin/_cctally_config.py +50 -0
- package/bin/_cctally_core.py +7 -4
- package/bin/_cctally_dashboard.py +352 -58
- package/bin/_cctally_dashboard_conversation.py +4 -3
- package/bin/_cctally_dashboard_envelope.py +12 -0
- package/bin/_cctally_dashboard_sources.py +25 -3
- package/bin/_cctally_doctor.py +35 -4
- package/bin/_cctally_milestone_history.py +845 -0
- package/bin/_cctally_statusline.py +128 -3
- package/bin/_cctally_tui.py +23 -0
- package/bin/_cctally_update.py +338 -15
- package/bin/_cctally_weekrefs.py +6 -2
- package/bin/_lib_codex_conversation.py +1342 -18
- package/bin/_lib_codex_conversation_export.py +21 -1
- package/bin/_lib_codex_conversation_query.py +530 -67
- package/bin/_lib_conversation_dispatch.py +2 -2
- package/bin/_lib_dashboard_json.py +43 -0
- package/bin/_lib_doctor.py +38 -0
- package/bin/_lib_milestone_history.py +158 -0
- package/bin/_lib_source_analytics.py +20 -6
- package/bin/cctally +36 -0
- package/dashboard/static/assets/index-BwvAcS_k.js +92 -0
- package/dashboard/static/assets/index-CrIlpAlQ.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-CvcCeA4L.css +0 -1
- package/dashboard/static/assets/index-DLNXAevv.js +0 -87
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.79.0] - 2026-07-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- The dashboard hero modal (Current Week / Current Cycle) now navigates history: the week/cycle chip's `‹`/`›` (or `ArrowUp`/`ArrowDown`) steps between previous 7-day billing cycles and a block navigator (or `ArrowLeft`/`ArrowRight`) scrolls a selected cycle's known 5-hour blocks, for both Claude subscription weeks and Codex native quota cycles; the default view stays on the current cycle with its active block. Weeks with a re-anchoring credit render the full chronological ledger with cumulative cost restarting per segment and a credit divider between segments, and empty weeks show an explicit empty state. A new `GET /api/milestones/<source>/week/<key>` route serves each selected week's complete milestone payload on demand, while a compact navigation index rides the live envelope (built only on non-idle snapshot rebuilds). Historic weeks are read-only; the Share action is hidden while a historic week is shown.
|
|
12
|
+
- Qualified Codex reasoning now uses a provider-native title/summary/body treatment instead of Claude Thinking chrome, while empty reasoning stays absent and Claude Thinking is unchanged. Evidence-preserved lifecycle fallbacks render as concise Codex task states, and recognized Git/memory harness markers become privacy-safe system actions with on-demand raw payload access; authored lookalikes remain literal prose. (#334)
|
|
13
|
+
- Qualified Codex conversation detail now preserves and renders plans, web searches, MCP completions, and agent-control operations as bounded provider-native cards with honest request/result/error state, safe web links, and on-demand raw payloads. Proven spawns gain a privacy-safe link to the exact retained child conversation only when same-root parent and canonical task-path evidence is unique; ambiguous, cross-root, malformed, and future shapes stay unlinked with the generic fallback. Local or placeholder Markdown image targets degrade to an explicit unavailable badge without a browser request or rendered path disclosure. (#332)
|
|
14
|
+
- Qualified Codex conversation detail now exposes bounded native terminal, output, and patch cards, and the dashboard renders supported `exec` calls as clean terminals plus `apply_patch`/patch-completion records as faithful diff cards. Provider names remain visible, diff-less events stay honest, raw payloads remain available on demand, and unsupported future wrappers keep the generic fallback. (#331)
|
|
15
|
+
- Beta release channel. Every release now ships to a beta channel first (npm `beta` dist-tag, GitHub prerelease); the maintainer later promotes chosen releases to stable (npm `latest`, GitHub "Latest", the Homebrew tap). Opt in with `cctally config set update.channel beta`: `cctally update` then tracks SemVer-max(beta, latest) and installs the exact resolved version (never a bare `@beta`), a `(beta)` marker appears on the CLI banner and the dashboard update badge/modal, the dashboard settings gain an Update-channel toggle, and `cctally doctor` reports the configured channel (warning on beta + Homebrew, which tracks stable only). Stable stays the default and flipping back to it never silently downgrades.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- The compact qualified conversation reader now keeps provider notification labels intact and the complete Codex input/output/cached-input/reasoning-output subtitle visible without clipping or horizontal overflow. Retained Codex world-state, inter-agent metadata, turn-context control fields, and unknown future record families stay replay-safe but no longer risk becoming fabricated transcript noise or exposing private harness state. (#335)
|
|
19
|
+
- The dashboard's qualified conversation reader now keeps Codex reasoning, tool calls/results, injected metadata labels, and lifecycle events in their native semantic roles instead of collapsing them into generic background rows. Skill-invocation titles no longer expose local `SKILL.md` paths, the outline prioritizes real prompts, logical assistant responses, and compactions while retaining full detail payload, token, model, and cost data, and an overflow menu opened immediately after reader mount is no longer closed by delayed initialization. (#330)
|
|
20
|
+
|
|
21
|
+
## [1.78.0] - 2026-07-21
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Dashboard Codex Daily, Weekly, and Monthly details now retain native Input, Cached input, Output, Reasoning, and Total counters; Codex Weekly uses reset-cycle vocabulary, All Weekly uses neutral provider-period wording, and partial-metadata projects with the same basename remain collision-safe without exposing identities. (#329)
|
|
25
|
+
- Dashboard Codex session, project, and quota-block details now use bounded native hierarchies, localized timestamps, retained quota progression, and accessible prompt expansion across desktop and mobile. (#325)
|
|
26
|
+
- Dashboard All-mode Claude project and session rows now open the same useful detail hierarchy and cross-navigation as the canonical Claude views while keeping native session IDs and source paths private. (#325)
|
|
27
|
+
- Dashboard All-mode Forecast and Cache Report cards and details now compose separate, provider-labelled Claude and Codex reports with provider-local values, confidence, and degraded or unavailable reasons instead of presenting Codex-only data under an All heading. (#324)
|
|
28
|
+
- Dashboard All-mode Weekly and `$ / 1% Trend` views now keep Claude and Codex quota histories in separate provider-labelled sections and series, while explicitly combining only compatible cost totals. (#324)
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Dashboard narrow layouts now keep degraded source status readable, hide the optional Codex five-hour slot when no native 300-minute window is reported, label real All-mode five-hour rows by provider, and bound unavailable combined-total warnings without losing their accessible detail. (#329)
|
|
32
|
+
- Dashboard source-wide freshness and read-model warnings now reach Daily and Projects without hiding the fixed ten-card board; Current Usage and Share stay bound to the source that opened them, and All shows separate Claude and Codex current-cycle sections instead of silently falling back to Claude. (#328)
|
|
33
|
+
- Dashboard HTTP and live-event JSON now convert non-finite numeric values to `null` at one strict outbound boundary, so missing Doctor freshness markers no longer emit browser-invalid `Infinity` tokens and the full Doctor report opens normally. (#327)
|
|
34
|
+
- Claude 5-hour and 7-day usage now receive one account-wide authoritative OAuth confirmation per 30-second status-line cycle, so a stale Claude Code `rate_limits` payload can no longer leave dashboard milestones behind for up to five minutes. The confirmation is detached from rendering, shares the existing hook throttle lock, and preserves `Retry-After` plus exponential `429` backoff.
|
|
35
|
+
|
|
8
36
|
## [1.77.0] - 2026-07-21
|
|
9
37
|
|
|
10
38
|
### Added
|
package/README.md
CHANGED
|
@@ -49,6 +49,8 @@ cctally dashboard # opens http://127.0.0.1:8789
|
|
|
49
49
|
|
|
50
50
|
For status-line integration and tuning, see [docs/installation.md](docs/installation.md) and [docs/configuration.md](docs/configuration.md).
|
|
51
51
|
|
|
52
|
+
**Beta channel (opt-in).** Every release ships to a beta channel first; the maintainer promotes the ones that prove out to stable, which is the default. To ride along with the newest builds as they land, opt in with `cctally config set update.channel beta` (npm or source installs — Homebrew tracks stable). `cctally update` then installs the exact latest beta version, and `cctally config set update.channel stable` flips you back cleanly with no silent downgrade. See [docs/commands/update.md](docs/commands/update.md#beta-channel).
|
|
53
|
+
|
|
52
54
|
## The live dashboard
|
|
53
55
|
|
|
54
56
|
`cctally dashboard` serves a web app at `localhost:8789` that updates live as you work - no refresh, no polling. Eleven panels cover the whole picture: **current week**, **forecast**, **$/1% trend**, **recent sessions**, **weekly**, **monthly**, **5-hour blocks**, **daily heatmap**, **projects**, **cache report**, and **recent alerts**. Any panel expands into a focused view, sessions are filterable and searchable, and a settings drawer tunes alerts and display options on the fly. It runs only on your own machine by default; one flag opens it to the other devices on your network when you want that.
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -1044,6 +1044,52 @@ def _replay_codex_normalization(conn: sqlite3.Connection) -> None:
|
|
|
1044
1044
|
_recompute_codex_rollups(conn, affected)
|
|
1045
1045
|
|
|
1046
1046
|
|
|
1047
|
+
def _repair_codex_turn_ids_for_source(
|
|
1048
|
+
conn: sqlite3.Connection, source_path: str,
|
|
1049
|
+
) -> set[str]:
|
|
1050
|
+
"""Reconcile stored normalized rows after a late native turn anchor arrives.
|
|
1051
|
+
|
|
1052
|
+
Active Codex streams can emit the response before a later completion, patch,
|
|
1053
|
+
or abort record exposes its turn id. Run this bounded per-file repair only
|
|
1054
|
+
when such a native proof arrives; ordinary append ticks remain delta-only.
|
|
1055
|
+
The physical log stays authoritative.
|
|
1056
|
+
"""
|
|
1057
|
+
events = [
|
|
1058
|
+
_lib_jsonl.CodexPhysicalEvent(*row)
|
|
1059
|
+
for row in conn.execute(
|
|
1060
|
+
"SELECT source_path, line_offset, source_root_key, conversation_key, "
|
|
1061
|
+
"native_thread_id, root_thread_id, parent_thread_id, timestamp_utc, "
|
|
1062
|
+
"record_type, event_type, turn_id, call_id, payload_json "
|
|
1063
|
+
"FROM codex_conversation_events WHERE source_path=? ORDER BY line_offset",
|
|
1064
|
+
(source_path,),
|
|
1065
|
+
)
|
|
1066
|
+
]
|
|
1067
|
+
inferred, _terminal = _lib_codex_conversation.infer_codex_event_turns(events)
|
|
1068
|
+
expected = {event.line_offset: turn for event, turn in zip(events, inferred)}
|
|
1069
|
+
affected = {
|
|
1070
|
+
row[0]
|
|
1071
|
+
for row in conn.execute(
|
|
1072
|
+
"SELECT DISTINCT conversation_key FROM codex_conversation_messages "
|
|
1073
|
+
"WHERE source_path=?",
|
|
1074
|
+
(source_path,),
|
|
1075
|
+
)
|
|
1076
|
+
if row[0]
|
|
1077
|
+
}
|
|
1078
|
+
for line_offset, stored_turn in conn.execute(
|
|
1079
|
+
"SELECT line_offset, turn_id FROM codex_conversation_messages "
|
|
1080
|
+
"WHERE source_path=?",
|
|
1081
|
+
(source_path,),
|
|
1082
|
+
):
|
|
1083
|
+
inferred_turn = expected.get(line_offset)
|
|
1084
|
+
if stored_turn != inferred_turn:
|
|
1085
|
+
conn.execute(
|
|
1086
|
+
"UPDATE codex_conversation_messages SET turn_id=? "
|
|
1087
|
+
"WHERE source_path=? AND line_offset=?",
|
|
1088
|
+
(inferred_turn, source_path, line_offset),
|
|
1089
|
+
)
|
|
1090
|
+
return affected
|
|
1091
|
+
|
|
1092
|
+
|
|
1047
1093
|
def _collect_inactive_codex_paths_and_roots(
|
|
1048
1094
|
conn: sqlite3.Connection,
|
|
1049
1095
|
current_file_identities: set[tuple[str, str]],
|
|
@@ -4849,6 +4895,7 @@ def open_conversations_db(*, attach_cache: bool = True) -> sqlite3.Connection:
|
|
|
4849
4895
|
cache_uri = _cctally_core.CACHE_DB_PATH.resolve().as_uri() + "?mode=ro"
|
|
4850
4896
|
conn.execute("ATTACH DATABASE ? AS cache_db", (cache_uri,))
|
|
4851
4897
|
_import_legacy_conversation_rows(conn)
|
|
4898
|
+
_ensure_codex_conversation_contract(conn)
|
|
4852
4899
|
_harden_conversation_sidecars()
|
|
4853
4900
|
return conn
|
|
4854
4901
|
|
|
@@ -4900,6 +4947,68 @@ def _import_legacy_conversation_rows(conn: sqlite3.Connection) -> None:
|
|
|
4900
4947
|
conn.commit()
|
|
4901
4948
|
|
|
4902
4949
|
|
|
4950
|
+
def _ensure_codex_conversation_contract(conn: sqlite3.Connection) -> bool:
|
|
4951
|
+
"""Converge retained Codex events on first read after a contract bump.
|
|
4952
|
+
|
|
4953
|
+
Qualified CLI reads and ``dashboard --no-sync`` intentionally do not ingest
|
|
4954
|
+
JSONL. They still must remain usable after an upgrade, so replay only the
|
|
4955
|
+
already-retained physical events under the provider-local conversation lock.
|
|
4956
|
+
Empty stores keep their existing rebuild marker for the next real sync.
|
|
4957
|
+
"""
|
|
4958
|
+
current = _lib_codex_conversation.CODEX_CONVERSATION_CONTRACT_VERSION
|
|
4959
|
+
|
|
4960
|
+
def needs_replay() -> bool:
|
|
4961
|
+
version = conn.execute(
|
|
4962
|
+
"SELECT value FROM cache_meta "
|
|
4963
|
+
"WHERE key='codex_conversation_contract_version'"
|
|
4964
|
+
).fetchone()
|
|
4965
|
+
pending = conn.execute(
|
|
4966
|
+
"SELECT 1 FROM cache_meta "
|
|
4967
|
+
"WHERE key='conversation_rebuild_codex_pending'"
|
|
4968
|
+
).fetchone() is not None
|
|
4969
|
+
has_events = conn.execute(
|
|
4970
|
+
"SELECT 1 FROM codex_conversation_events LIMIT 1"
|
|
4971
|
+
).fetchone() is not None
|
|
4972
|
+
return has_events and (pending or version is None or version[0] != current)
|
|
4973
|
+
|
|
4974
|
+
if not needs_replay():
|
|
4975
|
+
return False
|
|
4976
|
+
|
|
4977
|
+
_cctally_core.CONVERSATIONS_LOCK_CODEX_PATH.touch()
|
|
4978
|
+
lock_fh = open(_cctally_core.CONVERSATIONS_LOCK_CODEX_PATH, "w")
|
|
4979
|
+
try:
|
|
4980
|
+
if not _acquire_cache_flock(lock_fh, timeout=15.0):
|
|
4981
|
+
return False
|
|
4982
|
+
if not needs_replay():
|
|
4983
|
+
return False
|
|
4984
|
+
try:
|
|
4985
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
4986
|
+
_codex_conversation_fts_full_clear(conn)
|
|
4987
|
+
_replay_codex_normalization(conn)
|
|
4988
|
+
conn.execute(
|
|
4989
|
+
"INSERT OR REPLACE INTO cache_meta(key,value) VALUES(?,?)",
|
|
4990
|
+
("codex_conversation_contract_version", current),
|
|
4991
|
+
)
|
|
4992
|
+
conn.execute(
|
|
4993
|
+
"DELETE FROM cache_meta "
|
|
4994
|
+
"WHERE key='conversation_rebuild_codex_pending'"
|
|
4995
|
+
)
|
|
4996
|
+
conn.commit()
|
|
4997
|
+
return True
|
|
4998
|
+
except (sqlite3.DatabaseError, OSError, ValueError) as exc:
|
|
4999
|
+
conn.rollback()
|
|
5000
|
+
eprint(
|
|
5001
|
+
f"[codex-conversations] retained-event contract replay failed: {exc}"
|
|
5002
|
+
)
|
|
5003
|
+
return False
|
|
5004
|
+
finally:
|
|
5005
|
+
try:
|
|
5006
|
+
fcntl.flock(lock_fh, fcntl.LOCK_UN)
|
|
5007
|
+
except OSError:
|
|
5008
|
+
pass
|
|
5009
|
+
lock_fh.close()
|
|
5010
|
+
|
|
5011
|
+
|
|
4903
5012
|
def _prepare_claude_conversation_maintenance(
|
|
4904
5013
|
conn: sqlite3.Connection,
|
|
4905
5014
|
*,
|
|
@@ -5240,10 +5349,25 @@ def sync_codex_conversations(
|
|
|
5240
5349
|
"SELECT 1 FROM cache_meta "
|
|
5241
5350
|
"WHERE key='conversation_rebuild_codex_pending'"
|
|
5242
5351
|
).fetchone() is not None
|
|
5243
|
-
|
|
5352
|
+
contract_row = conn.execute(
|
|
5353
|
+
"SELECT value FROM cache_meta "
|
|
5354
|
+
"WHERE key='codex_conversation_contract_version'"
|
|
5355
|
+
).fetchone()
|
|
5356
|
+
has_contract_state = conn.execute(
|
|
5357
|
+
"SELECT 1 FROM codex_conversation_events LIMIT 1"
|
|
5358
|
+
).fetchone() is not None
|
|
5359
|
+
contract_rebuild = (
|
|
5360
|
+
has_contract_state
|
|
5361
|
+
and (
|
|
5362
|
+
contract_row is None
|
|
5363
|
+
or contract_row[0]
|
|
5364
|
+
!= _lib_codex_conversation.CODEX_CONVERSATION_CONTRACT_VERSION
|
|
5365
|
+
)
|
|
5366
|
+
)
|
|
5367
|
+
if (pending_rebuild or contract_rebuild) and targeted:
|
|
5244
5368
|
stats.deferred_reason = "rebuild_pending"
|
|
5245
5369
|
return stats
|
|
5246
|
-
rebuild = rebuild or pending_rebuild
|
|
5370
|
+
rebuild = rebuild or pending_rebuild or contract_rebuild
|
|
5247
5371
|
if rebuild:
|
|
5248
5372
|
_clear_codex_conversation_store(conn)
|
|
5249
5373
|
conn.commit()
|
|
@@ -5464,6 +5588,13 @@ def sync_codex_conversations(
|
|
|
5464
5588
|
affected_keys.update(
|
|
5465
5589
|
row.conversation_key for row in normalized.rows
|
|
5466
5590
|
)
|
|
5591
|
+
if any(
|
|
5592
|
+
_lib_codex_conversation.codex_event_is_late_turn_anchor(event)
|
|
5593
|
+
for event in events
|
|
5594
|
+
):
|
|
5595
|
+
affected_keys.update(
|
|
5596
|
+
_repair_codex_turn_ids_for_source(conn, path_str)
|
|
5597
|
+
)
|
|
5467
5598
|
_recompute_codex_rollups(conn, affected_keys)
|
|
5468
5599
|
terminal = state.thread
|
|
5469
5600
|
conn.execute(
|
|
@@ -5511,6 +5642,13 @@ def sync_codex_conversations(
|
|
|
5511
5642
|
stats.files_failed += 1
|
|
5512
5643
|
|
|
5513
5644
|
if only_paths is None and stats.files_failed == 0:
|
|
5645
|
+
conn.execute(
|
|
5646
|
+
"INSERT OR REPLACE INTO cache_meta(key,value) VALUES(?,?)",
|
|
5647
|
+
(
|
|
5648
|
+
"codex_conversation_contract_version",
|
|
5649
|
+
_lib_codex_conversation.CODEX_CONVERSATION_CONTRACT_VERSION,
|
|
5650
|
+
),
|
|
5651
|
+
)
|
|
5514
5652
|
conn.execute(
|
|
5515
5653
|
"DELETE FROM cache_meta "
|
|
5516
5654
|
"WHERE key='conversation_rebuild_codex_pending'"
|
package/bin/_cctally_config.py
CHANGED
|
@@ -309,6 +309,7 @@ ALLOWED_CONFIG_KEYS = (
|
|
|
309
309
|
"dashboard.live_tail",
|
|
310
310
|
"update.check.enabled",
|
|
311
311
|
"update.check.ttl_hours",
|
|
312
|
+
"update.channel",
|
|
312
313
|
"statusline.visual_burn_rate",
|
|
313
314
|
"statusline.cost_source",
|
|
314
315
|
"statusline.cctally_extensions",
|
|
@@ -883,6 +884,12 @@ def _config_known_value(config: dict, key: str) -> "object":
|
|
|
883
884
|
return c._validate_update_check_ttl_hours_value(stored)
|
|
884
885
|
except ValueError:
|
|
885
886
|
return c.UPDATE_DEFAULT_TTL_HOURS
|
|
887
|
+
if key == "update.channel":
|
|
888
|
+
# Release channel opt-in (beta-channel, spec 2026-07-21 §3). Default
|
|
889
|
+
# "stable"; hand-edited junk surfaces the default — mirrors
|
|
890
|
+
# dashboard.bind / update.check.*. The single resolver keeps the CLI,
|
|
891
|
+
# check pipeline, doctor, and dashboard envelope in lockstep.
|
|
892
|
+
return c.resolve_update_channel(config)
|
|
886
893
|
if key.startswith(_CODEX_BUDGET_LEAF_PREFIX):
|
|
887
894
|
return _config_codex_leaf_value(config, key)
|
|
888
895
|
if key in (
|
|
@@ -1564,6 +1571,35 @@ def _cmd_config_set(args: argparse.Namespace) -> int:
|
|
|
1564
1571
|
rendered = str(normalized)
|
|
1565
1572
|
print(f"{key}={rendered}")
|
|
1566
1573
|
return 0
|
|
1574
|
+
if key == "update.channel":
|
|
1575
|
+
# Release channel opt-in (beta-channel, spec 2026-07-21 §3). Enum
|
|
1576
|
+
# {stable,beta}; validate first so rejection short-circuits before
|
|
1577
|
+
# lock acquisition. Read-modify-write preserving the sibling
|
|
1578
|
+
# `update.check` block (mirrors update.check.* preserving nothing it
|
|
1579
|
+
# doesn't own).
|
|
1580
|
+
try:
|
|
1581
|
+
canonical = c._validate_update_channel_value(raw)
|
|
1582
|
+
except ValueError as exc:
|
|
1583
|
+
print(f"cctally config: {exc}", file=sys.stderr)
|
|
1584
|
+
return 2
|
|
1585
|
+
with config_writer_lock():
|
|
1586
|
+
config = _load_config_unlocked()
|
|
1587
|
+
existing_update = config.get("update")
|
|
1588
|
+
if existing_update is not None and not isinstance(existing_update, dict):
|
|
1589
|
+
print(
|
|
1590
|
+
"cctally: update config error: update must be an object",
|
|
1591
|
+
file=sys.stderr,
|
|
1592
|
+
)
|
|
1593
|
+
return 2
|
|
1594
|
+
update_block = dict(existing_update or {})
|
|
1595
|
+
update_block["channel"] = canonical
|
|
1596
|
+
config["update"] = update_block
|
|
1597
|
+
save_config(config)
|
|
1598
|
+
if getattr(args, "emit_json", False):
|
|
1599
|
+
print(json.dumps({"update": {"channel": canonical}}, indent=2))
|
|
1600
|
+
else:
|
|
1601
|
+
print(f"update.channel={canonical}")
|
|
1602
|
+
return 0
|
|
1567
1603
|
if key in (
|
|
1568
1604
|
"budget.weekly_usd",
|
|
1569
1605
|
"budget.alerts_enabled",
|
|
@@ -1990,6 +2026,20 @@ def _cmd_config_unset(args: argparse.Namespace) -> int:
|
|
|
1990
2026
|
save_config(config)
|
|
1991
2027
|
# idempotent: silent on missing key
|
|
1992
2028
|
return 0
|
|
2029
|
+
if key == "update.channel":
|
|
2030
|
+
# Drop the channel leaf; preserve a sibling update.check block, and
|
|
2031
|
+
# prune an empty `update` so config.json stays tidy. Mirrors the
|
|
2032
|
+
# update.check.* unset branch above.
|
|
2033
|
+
with config_writer_lock():
|
|
2034
|
+
config = _load_config_unlocked()
|
|
2035
|
+
update_block = config.get("update")
|
|
2036
|
+
if isinstance(update_block, dict) and "channel" in update_block:
|
|
2037
|
+
del update_block["channel"]
|
|
2038
|
+
if not update_block:
|
|
2039
|
+
config.pop("update", None)
|
|
2040
|
+
save_config(config)
|
|
2041
|
+
# idempotent: silent on missing key
|
|
2042
|
+
return 0
|
|
1993
2043
|
if key in (
|
|
1994
2044
|
"budget.weekly_usd",
|
|
1995
2045
|
"budget.alerts_enabled",
|
package/bin/_cctally_core.py
CHANGED
|
@@ -252,9 +252,11 @@ def _real_prod_data_dir() -> pathlib.Path:
|
|
|
252
252
|
# Internal (no config UI — YAGNI, spec §Out of scope); test injection only.
|
|
253
253
|
# Spec 2026-07-17-usage-statusline-fallback-design.
|
|
254
254
|
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
#
|
|
255
|
+
# STATUSLINE_OAUTH_POLL_SECONDS: the account-wide authoritative confirmation
|
|
256
|
+
# cadence driven by Claude Code's 30-second statusline timer. Keep it below
|
|
257
|
+
# the timer so scheduling jitter does not skip every other tick.
|
|
258
|
+
# OAUTH_BACKFILL_STALE_SECONDS: the matching selected-freshness gate shared by
|
|
259
|
+
# statusline-driven and hook-driven automatic OAuth refreshes.
|
|
258
260
|
# OAUTH_BACKOFF_BASE_SECONDS / OAUTH_BACKOFF_CAP_SECONDS: the headerless
|
|
259
261
|
# exponential 429 backoff (base * 2**consecutive_429, capped).
|
|
260
262
|
STATUSLINE_CANDIDATE_TTL_SECONDS = 90
|
|
@@ -270,7 +272,8 @@ STATUSLINE_TOMBSTONE_DOCUMENT_MAX_BYTES = 1024
|
|
|
270
272
|
# session waits on a long subagent (event-driven updates go quiet then). MUST
|
|
271
273
|
# Add-when-absent only; a user-set value is never mutated.
|
|
272
274
|
STATUSLINE_REFRESH_INTERVAL_DEFAULT = 30
|
|
273
|
-
|
|
275
|
+
STATUSLINE_OAUTH_POLL_SECONDS = 25.0
|
|
276
|
+
OAUTH_BACKFILL_STALE_SECONDS = STATUSLINE_OAUTH_POLL_SECONDS
|
|
274
277
|
OAUTH_BACKOFF_BASE_SECONDS = 60.0
|
|
275
278
|
OAUTH_BACKOFF_CAP_SECONDS = 3600.0
|
|
276
279
|
|