cc-session-tools 2.8.2__tar.gz → 2.10.0__tar.gz
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.
- {cc_session_tools-2.8.2/src/cc_session_tools.egg-info → cc_session_tools-2.10.0}/PKG-INFO +1 -1
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/pyproject.toml +1 -1
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccsched.py +14 -14
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccst.py +101 -2
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/init_service.py +57 -0
- cc_session_tools-2.10.0/src/cc_session_tools/lib/pdata/rename_group.py +161 -0
- cc_session_tools-2.10.0/src/cc_session_tools/lib/scheduler/digest.py +130 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/ledger.py +18 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/notify.py +35 -1
- cc_session_tools-2.10.0/src/cc_session_tools/lib/scheduler/surface.py +255 -0
- cc_session_tools-2.10.0/src/cc_session_tools/lib/scheduler/worker.py +235 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +17 -1
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0/src/cc_session_tools.egg-info}/PKG-INFO +1 -1
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/SOURCES.txt +2 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/catchup.py +27 -2
- cc_session_tools-2.10.0/tests/test_ccst_pdata_rename_group_cli.py +117 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_verify_cli.py +41 -1
- cc_session_tools-2.8.2/src/cc_session_tools/lib/scheduler/digest.py +0 -93
- cc_session_tools-2.8.2/src/cc_session_tools/lib/scheduler/surface.py +0 -177
- cc_session_tools-2.8.2/src/cc_session_tools/lib/scheduler/worker.py +0 -133
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/LICENSE +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/README.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/setup.cfg +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccd.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccmsg.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccr.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccs.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/config/hooks-bundle.json +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/config/session-tag-hook.json +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/hooks_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/claude_flags.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/claude_md_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/context_overrides.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/debug.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/doctor.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/hook_registry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/install_sync.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/levenshtein.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/lock.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/message.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/repository.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/retention.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/service.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/paths.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/backup.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/classify.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/importers.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/naming.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/repository.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/service.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/verify.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/picker.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/proc_lock.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/prompts.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/roots.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/rules.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/due.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/state.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/session_gc.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/sessions.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/sessions_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/sessions_repair.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/shell_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/tasklist.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/telemetry_store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-pdata-conflict-resolution/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-pdata-schema-design/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-project-init/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/requires.txt +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/top_level.txt +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/after_response.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/bash_hard_deny.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/bash_security_review.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/confirm_8digit.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/context_window_warning.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/last_screenshot.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/marker_allow.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/markers.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/messaging_deliver.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/model_info.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/normalise.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/pending_migration.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/pending_rename.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/session_tag.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/stats.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/telemetry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/telemetry_query.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/telemetry_trim.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/transcript.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/worklog_guard.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/attribution.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/ccusage_wrapper.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/data/pricing.json +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/parent_inference.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/parser.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/pricing.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/query.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/report.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/schema.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/session_names.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_after_response.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_attribution.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_bash_hard_deny.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_bash_security_review.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cache_sqlite.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccr_orphans.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_emptiness.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_eta.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_list_mode.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_scope_flags.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_sentinel_sort.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_session_counts.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_bundle_discovery.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_context_override_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_doctor.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_gc_report.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_hook_dispatcher.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_hooks_install_prune.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_hooks_install_zero_arg.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_hooks_uninstall.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_install_everything.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_main_gate.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_init_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_repair_sessions_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_sessions_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_shell_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_skills_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_skills_uninstall.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_telemetry_query.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_telemetry_trim.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccusage_wrapper.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_claude_flags.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_claude_md_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_ccd.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_ccd_dry_run.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_ccr.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_ccs.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_version.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_confirm_8digit.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_context_overrides.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_context_window_warning.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_debug.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_doctor_mutes.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_empty_session.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_hook_cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_hooks_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_install_sync.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_last_screenshot.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_levenshtein.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_marker_allow.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_migrate_ccmsg.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_migrate_ccsched.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_migrate_sessions_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_migrate_telemetry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_model_info.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_normalise.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_parent_inference.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_parser.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_paths.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_pending_migration_hook.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_picker.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_pricing.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_proc_lock.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_prompts.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_query.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_report.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_roots.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_rules.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_scheduler_bundled_jobs.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_schema.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_session_names.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_session_tag.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_sessions.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_sessions_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_sessions_repair.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_tasklist.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_telemetry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_telemetry_store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_transcript.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_worklog_guard.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cc-session-tools"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.10.0"
|
|
8
8
|
description = "Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (cccs_hooks)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -11,6 +11,7 @@ from cc_session_tools import __version__
|
|
|
11
11
|
from cc_session_tools.lib.scheduler import (
|
|
12
12
|
cursor,
|
|
13
13
|
ledger,
|
|
14
|
+
notify,
|
|
14
15
|
reconcile,
|
|
15
16
|
registry,
|
|
16
17
|
state,
|
|
@@ -240,7 +241,9 @@ def _cmd_remove(job_id: str) -> int:
|
|
|
240
241
|
return 0
|
|
241
242
|
|
|
242
243
|
|
|
243
|
-
def _cmd_run(
|
|
244
|
+
def _cmd_run(
|
|
245
|
+
args: argparse.Namespace, *, notify_push: worker.NotifyPush = notify.push_outcome,
|
|
246
|
+
) -> int:
|
|
244
247
|
specs = {s.job_id: s for s in registry.load_registry()}
|
|
245
248
|
spec: JobSpec | None = specs.get(args.id)
|
|
246
249
|
if spec is None:
|
|
@@ -249,25 +252,22 @@ def _cmd_run(args: argparse.Namespace) -> int:
|
|
|
249
252
|
now = datetime.now(timezone.utc)
|
|
250
253
|
attempt_ts = state.format_ts(now)
|
|
251
254
|
state.ensure_registered_db(spec.job_id, now)
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
# Manual runs always use record_manual_failure/record_success, not
|
|
256
|
+
# worker.py's suspend-threshold-aware record_failure - `ccsched run` never
|
|
257
|
+
# auto-suspends a job, unlike the scheduled `_run-job` path.
|
|
258
|
+
capture = worker.classify_outcome(spec, outcome, notify_push=notify_push)
|
|
259
|
+
if capture.crashed:
|
|
254
260
|
new_consecutive = state.record_manual_failure(spec.job_id, attempt_ts=attempt_ts)
|
|
255
261
|
else:
|
|
256
262
|
state.record_success(spec.job_id, new_success=attempt_ts, attempt_ts=attempt_ts)
|
|
257
263
|
new_consecutive = 0
|
|
258
|
-
findings = None
|
|
259
|
-
if not crashed and outcome.exit_code != 0:
|
|
260
|
-
findings = outcome.stdout.strip()[:1000] or None
|
|
261
264
|
ledger.record(ledger.LedgerEntry(
|
|
262
|
-
job_id=spec.job_id,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
duration_ms=outcome.duration_ms,
|
|
266
|
-
error=(outcome.stderr.strip()[:200] or None) if crashed else findings,
|
|
267
|
-
consecutive_failures=new_consecutive if crashed else 0,
|
|
265
|
+
job_id=spec.job_id, event=capture.event, owed=1, ran=0 if capture.crashed else 1,
|
|
266
|
+
exit_code=outcome.exit_code, duration_ms=outcome.duration_ms, error=capture.detail,
|
|
267
|
+
consecutive_failures=new_consecutive if capture.crashed else 0,
|
|
268
268
|
))
|
|
269
|
-
print(f"{'failed' if crashed else 'ran'} {spec.job_id} (exit={outcome.exit_code})")
|
|
270
|
-
return 1 if crashed else 0
|
|
269
|
+
print(f"{'failed' if capture.crashed else 'ran'} {spec.job_id} (exit={outcome.exit_code})")
|
|
270
|
+
return 1 if capture.crashed else 0
|
|
271
271
|
|
|
272
272
|
|
|
273
273
|
def _cmd_status(args: argparse.Namespace) -> int:
|
|
@@ -1217,6 +1217,64 @@ def _cmd_pdata_reorganize(args: argparse.Namespace) -> int:
|
|
|
1217
1217
|
return 0
|
|
1218
1218
|
|
|
1219
1219
|
|
|
1220
|
+
def _cmd_pdata_rename_group(args: argparse.Namespace) -> int:
|
|
1221
|
+
from cc_session_tools.lib.pdata import init_paths, naming, rename_group
|
|
1222
|
+
|
|
1223
|
+
try:
|
|
1224
|
+
project_root = init_paths.resolve_project_root(args.project, rehearse=None)
|
|
1225
|
+
except ValueError as exc:
|
|
1226
|
+
print(f"ccst pdata: {exc}", file=sys.stderr)
|
|
1227
|
+
return 2
|
|
1228
|
+
|
|
1229
|
+
if not args.write:
|
|
1230
|
+
try:
|
|
1231
|
+
plan = rename_group.dry_run(
|
|
1232
|
+
project=args.project, project_root=project_root, old=args.from_, new=args.to,
|
|
1233
|
+
)
|
|
1234
|
+
except ValueError as exc:
|
|
1235
|
+
print(f"ccst pdata: {exc}", file=sys.stderr)
|
|
1236
|
+
return 2
|
|
1237
|
+
print(f"record_group: {plan.old} -> {plan.new}")
|
|
1238
|
+
print(f" {plan.row_count} row(s) in records to update")
|
|
1239
|
+
if plan.has_extension_table:
|
|
1240
|
+
print(f" ext table: {naming.extension_table_name(plan.old)} -> "
|
|
1241
|
+
f"{naming.extension_table_name(plan.new)}")
|
|
1242
|
+
else:
|
|
1243
|
+
print(" no extension table")
|
|
1244
|
+
if plan.manifest_entry_paths:
|
|
1245
|
+
n = len(plan.manifest_entry_paths)
|
|
1246
|
+
print(f" {n} manifest entr{'y' if n == 1 else 'ies'} to update:")
|
|
1247
|
+
for path in plan.manifest_entry_paths:
|
|
1248
|
+
print(f" {path}")
|
|
1249
|
+
else:
|
|
1250
|
+
print(" no matching manifest entries")
|
|
1251
|
+
return 0
|
|
1252
|
+
|
|
1253
|
+
try:
|
|
1254
|
+
result = rename_group.write(
|
|
1255
|
+
project=args.project, project_root=project_root, old=args.from_, new=args.to,
|
|
1256
|
+
)
|
|
1257
|
+
except ValueError as exc:
|
|
1258
|
+
print(f"ccst pdata: {exc}", file=sys.stderr)
|
|
1259
|
+
return 2
|
|
1260
|
+
|
|
1261
|
+
if result.failure is not None:
|
|
1262
|
+
print("ccst pdata rename-group: failed:", file=sys.stderr)
|
|
1263
|
+
for reason in result.failure.reasons:
|
|
1264
|
+
print(f" - {reason}", file=sys.stderr)
|
|
1265
|
+
return 1
|
|
1266
|
+
|
|
1267
|
+
plan = result.plan
|
|
1268
|
+
print(f"Renamed record_group {plan.old!r} -> {plan.new!r}: {plan.row_count} row(s)")
|
|
1269
|
+
print(f"Backup: {result.backup_path}")
|
|
1270
|
+
n = len(plan.manifest_entry_paths)
|
|
1271
|
+
if n:
|
|
1272
|
+
print(f"{n} manifest entr{'y' if n == 1 else 'ies'} updated - remember to update any "
|
|
1273
|
+
f"skills/docs that still reference the old group name.")
|
|
1274
|
+
print("SUCCESS")
|
|
1275
|
+
return 0
|
|
1276
|
+
|
|
1277
|
+
|
|
1220
1278
|
def _cmd_pdata_reconcile_session_output(args: argparse.Namespace) -> int:
|
|
1221
1279
|
from cc_session_tools.lib import roots
|
|
1222
1280
|
from cc_session_tools.lib.pdata import session_output
|
|
@@ -1274,7 +1332,16 @@ def _cmd_pdata_verify(args: argparse.Namespace) -> int:
|
|
|
1274
1332
|
print("ccst pdata verify: no project databases found", file=sys.stderr)
|
|
1275
1333
|
return 2
|
|
1276
1334
|
|
|
1335
|
+
# --all-projects without --verbose collapses the per-project listing to
|
|
1336
|
+
# one summary line - with N projects, a line each (even when every one
|
|
1337
|
+
# is clean) was the "very long" default output a daily unattended job
|
|
1338
|
+
# printed into every catch-up digest. --project already prints just one
|
|
1339
|
+
# project's worth, which isn't that problem, so the split only applies
|
|
1340
|
+
# to --all-projects; a single --project run always gets full detail.
|
|
1341
|
+
compact = args.all_projects and not args.verbose
|
|
1342
|
+
|
|
1277
1343
|
worst = 0
|
|
1344
|
+
flagged: list[str] = []
|
|
1278
1345
|
for project in projects:
|
|
1279
1346
|
try:
|
|
1280
1347
|
summary = verify.run_verify(project=project, full=args.full)
|
|
@@ -1282,11 +1349,24 @@ def _cmd_pdata_verify(args: argparse.Namespace) -> int:
|
|
|
1282
1349
|
print(f"ccst pdata verify: {project}: {exc}", file=sys.stderr)
|
|
1283
1350
|
worst = max(worst, 2)
|
|
1284
1351
|
continue
|
|
1352
|
+
if summary.status != "OK":
|
|
1353
|
+
worst = max(worst, 1)
|
|
1354
|
+
flagged.append(project)
|
|
1355
|
+
if compact:
|
|
1356
|
+
continue
|
|
1285
1357
|
print(f"{project}: {summary.status} ({len(summary.issues)} issue(s))")
|
|
1286
1358
|
for issue in summary.issues:
|
|
1287
1359
|
print(f" [{issue.severity}] {issue.check}: {issue.message}")
|
|
1288
|
-
|
|
1289
|
-
|
|
1360
|
+
|
|
1361
|
+
if compact:
|
|
1362
|
+
if flagged:
|
|
1363
|
+
print(
|
|
1364
|
+
f"ccst pdata verify --all-projects: ISSUES in {len(flagged)} of "
|
|
1365
|
+
f"{len(projects)} project(s) - run 'ccst pdata verify --all-projects "
|
|
1366
|
+
f"--verbose' for details"
|
|
1367
|
+
)
|
|
1368
|
+
else:
|
|
1369
|
+
print(f"ccst pdata verify --all-projects: OK ({len(projects)} project(s), 0 issue(s))")
|
|
1290
1370
|
return worst
|
|
1291
1371
|
|
|
1292
1372
|
|
|
@@ -2247,6 +2327,18 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
2247
2327
|
help="Perform the move and update matching pdata records (default: dry-run only)",
|
|
2248
2328
|
)
|
|
2249
2329
|
|
|
2330
|
+
pdata_rename_group_parser = pdata_sub.add_parser(
|
|
2331
|
+
"rename-group", help="Rename a record_group in place (records, record_group_fields, "
|
|
2332
|
+
"its ext table, and any matching manifest entries)"
|
|
2333
|
+
)
|
|
2334
|
+
pdata_rename_group_parser.add_argument("--project", required=True, metavar="NAME")
|
|
2335
|
+
pdata_rename_group_parser.add_argument("--from", dest="from_", required=True, metavar="OLD")
|
|
2336
|
+
pdata_rename_group_parser.add_argument("--to", required=True, metavar="NEW")
|
|
2337
|
+
pdata_rename_group_parser.add_argument(
|
|
2338
|
+
"--write", action="store_true",
|
|
2339
|
+
help="Perform the rename and update .ccst-pdata-proposal.json (default: dry-run only)",
|
|
2340
|
+
)
|
|
2341
|
+
|
|
2250
2342
|
pdata_reconcile_parser = pdata_sub.add_parser(
|
|
2251
2343
|
"reconcile-session-output",
|
|
2252
2344
|
help="Backfill the session-output index from cc-sessions/*/out/ on disk (idempotent)",
|
|
@@ -2277,6 +2369,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
2277
2369
|
"--full", action="store_true",
|
|
2278
2370
|
help="Rescan every row instead of only rows changed since the last run",
|
|
2279
2371
|
)
|
|
2372
|
+
pdata_verify_parser.add_argument(
|
|
2373
|
+
"--verbose", action="store_true",
|
|
2374
|
+
help="With --all-projects, print the full per-project listing instead of "
|
|
2375
|
+
"the default one-line summary (--project always prints full detail)",
|
|
2376
|
+
)
|
|
2280
2377
|
|
|
2281
2378
|
# ---- sessions ----
|
|
2282
2379
|
sessions_parser = sub.add_parser("sessions", help="sessions.db management commands")
|
|
@@ -2553,6 +2650,8 @@ def main() -> None:
|
|
|
2553
2650
|
sys.exit(_cmd_pdata_init(args))
|
|
2554
2651
|
if args.verb == "reorganize":
|
|
2555
2652
|
sys.exit(_cmd_pdata_reorganize(args))
|
|
2653
|
+
if args.verb == "rename-group":
|
|
2654
|
+
sys.exit(_cmd_pdata_rename_group(args))
|
|
2556
2655
|
if args.verb == "reconcile-session-output":
|
|
2557
2656
|
sys.exit(_cmd_pdata_reconcile_session_output(args))
|
|
2558
2657
|
if args.verb == "verify":
|
{cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/init_service.py
RENAMED
|
@@ -5,10 +5,12 @@ A's service.py — this module owns no SQL of its own.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import csv
|
|
8
|
+
import sqlite3
|
|
8
9
|
from collections.abc import Callable
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
13
|
+
from cc_session_tools.lib import db
|
|
12
14
|
from cc_session_tools.lib.pdata import (
|
|
13
15
|
backup,
|
|
14
16
|
cutover,
|
|
@@ -16,6 +18,7 @@ from cc_session_tools.lib.pdata import (
|
|
|
16
18
|
manifest,
|
|
17
19
|
repository,
|
|
18
20
|
service,
|
|
21
|
+
store,
|
|
19
22
|
)
|
|
20
23
|
from cc_session_tools.lib.pdata.importers import ImportRow, count_source_rows, import_entry
|
|
21
24
|
from cc_session_tools.lib.pdata.manifest import Manifest, ManifestEntry
|
|
@@ -137,6 +140,58 @@ def _rollback(*, project: str, created_ids: list[int]) -> list[str]:
|
|
|
137
140
|
return rollback_failures
|
|
138
141
|
|
|
139
142
|
|
|
143
|
+
def _check_db_not_locked(project: str) -> None:
|
|
144
|
+
"""Pre-flight concurrency guard (inter-session message 20260819T114156Z-3ec7,
|
|
145
|
+
gap #2): refuse to start --write if another connection already holds a write
|
|
146
|
+
lock on the project's .db right now — e.g. a concurrent --write, or
|
|
147
|
+
pdata-verify-all's scheduled cadence firing against the same project — rather
|
|
148
|
+
than letting the first schema_add_field/add_record call inside the with-block
|
|
149
|
+
below block on db.py's passive 5s busy_timeout and eventually fail with
|
|
150
|
+
rows already mid-migration. Called as the very first thing inside write()'s
|
|
151
|
+
project_db_dir_override/backup_dir_override with-block so store.db_path()
|
|
152
|
+
resolves the rehearsal-sandboxed path when rehearsing, exactly like every
|
|
153
|
+
other db access in this function.
|
|
154
|
+
|
|
155
|
+
No-op if the project has no .db yet (a brand-new project's first --write) —
|
|
156
|
+
nothing to guard, and connecting would create one just to probe it.
|
|
157
|
+
|
|
158
|
+
The probe is a throwaway BEGIN IMMEDIATE / ROLLBACK on its own short-lived
|
|
159
|
+
connection, not a lock held across the rest of write() — this does not
|
|
160
|
+
protect against a second --write starting a few seconds *after* this check
|
|
161
|
+
passes (a TOCTOU race that is out of scope; the existing passive
|
|
162
|
+
busy_timeout is still the backstop for that narrower case). Its job is only
|
|
163
|
+
to catch the common case of something *already* actively using the .db,
|
|
164
|
+
immediately and with a clear message, instead of a raw sqlite3 error
|
|
165
|
+
surfacing minutes into the migration.
|
|
166
|
+
|
|
167
|
+
busy_timeout is overridden to 0 on this connection only — db.connect()'s
|
|
168
|
+
shared 5000ms default is exactly the passive wait this probe exists to
|
|
169
|
+
avoid; a probe that waits out the same timeout the mutation path already
|
|
170
|
+
relies on would defeat the point of checking up front. isolation_level is
|
|
171
|
+
set to None for the same reason repository.connect() sets it: so this
|
|
172
|
+
caller-issued BEGIN IMMEDIATE isn't wrapped in a second, implicit
|
|
173
|
+
transaction by sqlite3's own legacy transaction handling."""
|
|
174
|
+
db_path = store.db_path(project)
|
|
175
|
+
if not db_path.exists():
|
|
176
|
+
return
|
|
177
|
+
conn = db.connect(db_path)
|
|
178
|
+
conn.isolation_level = None
|
|
179
|
+
try:
|
|
180
|
+
conn.execute("PRAGMA busy_timeout=0")
|
|
181
|
+
try:
|
|
182
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
183
|
+
except sqlite3.OperationalError as exc:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"cannot start 'ccst pdata init --write' for project {project!r}: "
|
|
186
|
+
f"another process appears to be using {db_path} right now ({exc}) "
|
|
187
|
+
f"— wait for it to finish, or confirm it isn't stuck, then re-run --write"
|
|
188
|
+
) from exc
|
|
189
|
+
else:
|
|
190
|
+
conn.execute("ROLLBACK")
|
|
191
|
+
finally:
|
|
192
|
+
conn.close()
|
|
193
|
+
|
|
194
|
+
|
|
140
195
|
def write(
|
|
141
196
|
*, project: str, rehearse: Path | None = None,
|
|
142
197
|
on_progress: Callable[[str], None] | None = None,
|
|
@@ -171,6 +226,8 @@ def write(
|
|
|
171
226
|
init_paths.project_db_dir_override(rehearse),
|
|
172
227
|
init_paths.backup_dir_override(rehearse),
|
|
173
228
|
):
|
|
229
|
+
_check_db_not_locked(project)
|
|
230
|
+
|
|
174
231
|
db_owned = [e for e in m.entries if e.classification == "db-owned"]
|
|
175
232
|
_emit(on_progress, f"Importing {len(db_owned)} file(s)...")
|
|
176
233
|
for entry in m.entries:
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Record-group rename for `ccst pdata rename-group` (feature request from the `home` project's
|
|
2
|
+
migration-test session, message 20260822T114514Z-91ed).
|
|
3
|
+
|
|
4
|
+
A record_group rename touches three places that must move together in one transaction -
|
|
5
|
+
`records.record_group`, `record_group_fields.record_group`, and the `ext_<group>` table's own
|
|
6
|
+
name - plus a fourth, non-transactional place that is just as easy to forget: any matching entry
|
|
7
|
+
in `.ccst-pdata-proposal.json` (the classification manifest, spec §7.1). Skip that and
|
|
8
|
+
verify.check_row_count_parity cross-checks live row counts against the manifest's archived-file
|
|
9
|
+
entries by record_group; every renamed group then fails `ccst pdata verify` forever with a
|
|
10
|
+
"possible data loss" row-count-parity issue that isn't actually data loss - it's just comparing
|
|
11
|
+
the new name's row count against the old name's manifest entry.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sqlite3
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from cc_session_tools.lib.pdata import backup, init_paths, manifest, naming, repository
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class RenameGroupPlan:
|
|
24
|
+
project: str
|
|
25
|
+
project_root: Path
|
|
26
|
+
old: str
|
|
27
|
+
new: str
|
|
28
|
+
row_count: int
|
|
29
|
+
has_extension_table: bool
|
|
30
|
+
manifest_entry_paths: list[str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class RenameGroupFailure:
|
|
35
|
+
reasons: list[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class RenameGroupResult:
|
|
40
|
+
plan: RenameGroupPlan
|
|
41
|
+
backup_path: Path | None
|
|
42
|
+
failure: RenameGroupFailure | None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _plan(*, project: str, project_root: Path, old: str, new: str) -> RenameGroupPlan:
|
|
46
|
+
naming.validate_record_group(old)
|
|
47
|
+
naming.validate_record_group(new)
|
|
48
|
+
if old == new:
|
|
49
|
+
raise ValueError(f"--from and --to are the same record_group: {old!r}")
|
|
50
|
+
|
|
51
|
+
conn = repository.connect(project)
|
|
52
|
+
try:
|
|
53
|
+
old_row_count = conn.execute(
|
|
54
|
+
"SELECT COUNT(*) FROM records WHERE record_group=?", (old,)
|
|
55
|
+
).fetchone()[0]
|
|
56
|
+
old_has_ext = repository.extension_table_exists(conn, old)
|
|
57
|
+
if old_row_count == 0 and not old_has_ext:
|
|
58
|
+
raise ValueError(f"no such record_group {old!r} in project {project!r}")
|
|
59
|
+
|
|
60
|
+
# "has rows" is checked including soft-deleted rows, and an ext table with zero rows
|
|
61
|
+
# (a schema-only group) also counts - both would otherwise collide silently with the
|
|
62
|
+
# ALTER TABLE RENAME TO / UPDATE below instead of raising a clear refusal up front.
|
|
63
|
+
new_row_count = conn.execute(
|
|
64
|
+
"SELECT COUNT(*) FROM records WHERE record_group=?", (new,)
|
|
65
|
+
).fetchone()[0]
|
|
66
|
+
new_has_ext = repository.extension_table_exists(conn, new)
|
|
67
|
+
if new_row_count > 0 or new_has_ext:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"record_group {new!r} already exists in project {project!r} - "
|
|
70
|
+
f"rename-group refuses to merge into an existing group"
|
|
71
|
+
)
|
|
72
|
+
finally:
|
|
73
|
+
conn.close()
|
|
74
|
+
|
|
75
|
+
proposal_path = project_root / init_paths.PROPOSAL_FILENAME
|
|
76
|
+
manifest_entry_paths: list[str] = []
|
|
77
|
+
if proposal_path.exists():
|
|
78
|
+
m = manifest.load(proposal_path)
|
|
79
|
+
manifest_entry_paths = [e.path for e in m.entries if e.record_group == old]
|
|
80
|
+
|
|
81
|
+
return RenameGroupPlan(
|
|
82
|
+
project=project, project_root=project_root, old=old, new=new,
|
|
83
|
+
row_count=old_row_count, has_extension_table=old_has_ext,
|
|
84
|
+
manifest_entry_paths=manifest_entry_paths,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def dry_run(*, project: str, project_root: Path, old: str, new: str) -> RenameGroupPlan:
|
|
89
|
+
return _plan(project=project, project_root=project_root, old=old, new=new)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _rename_in_db(project: str, *, old: str, new: str) -> None:
|
|
93
|
+
conn = repository.connect(project)
|
|
94
|
+
try:
|
|
95
|
+
with repository._immediate(conn):
|
|
96
|
+
if repository.extension_table_exists(conn, old):
|
|
97
|
+
old_table = naming.extension_table_name(old)
|
|
98
|
+
new_table = naming.extension_table_name(new)
|
|
99
|
+
conn.execute(f'ALTER TABLE "{old_table}" RENAME TO "{new_table}"')
|
|
100
|
+
conn.execute("UPDATE records SET record_group=? WHERE record_group=?", (new, old))
|
|
101
|
+
conn.execute(
|
|
102
|
+
"UPDATE record_group_fields SET record_group=? WHERE record_group=?", (new, old)
|
|
103
|
+
)
|
|
104
|
+
finally:
|
|
105
|
+
conn.close()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _rename_in_manifest(project_root: Path, *, old: str, new: str) -> int:
|
|
109
|
+
"""Returns the number of entries updated. No-op (returns 0) if the project was never
|
|
110
|
+
migrated - nothing to update against."""
|
|
111
|
+
proposal_path = project_root / init_paths.PROPOSAL_FILENAME
|
|
112
|
+
if not proposal_path.exists():
|
|
113
|
+
return 0
|
|
114
|
+
m = manifest.load(proposal_path)
|
|
115
|
+
updated = 0
|
|
116
|
+
for entry in m.entries:
|
|
117
|
+
if entry.record_group == old:
|
|
118
|
+
entry.record_group = new
|
|
119
|
+
updated += 1
|
|
120
|
+
if updated:
|
|
121
|
+
manifest.save(m, proposal_path)
|
|
122
|
+
return updated
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def write(*, project: str, project_root: Path, old: str, new: str) -> RenameGroupResult:
|
|
126
|
+
plan = _plan(project=project, project_root=project_root, old=old, new=new)
|
|
127
|
+
|
|
128
|
+
# Safety net before any write, matching reorganize.write() / init_service.write()'s own
|
|
129
|
+
# use of the same helper.
|
|
130
|
+
try:
|
|
131
|
+
backup_path = backup.create_backup(project=project, project_root=project_root)
|
|
132
|
+
except backup.BackupError as exc:
|
|
133
|
+
return RenameGroupResult(
|
|
134
|
+
plan=plan, backup_path=None, failure=RenameGroupFailure(reasons=[str(exc)]),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
_rename_in_db(project, old=old, new=new)
|
|
139
|
+
except sqlite3.Error as exc:
|
|
140
|
+
return RenameGroupResult(
|
|
141
|
+
plan=plan, backup_path=backup_path, failure=RenameGroupFailure(reasons=[str(exc)]),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# The DB rename already committed by this point - a manifest-write failure here (a
|
|
145
|
+
# permissions error, a malformed manifest another process wrote concurrently) must be
|
|
146
|
+
# reported clearly rather than silently leaving the manifest pointing at the old name,
|
|
147
|
+
# since that's exactly the "possible data loss" verify failure this command exists to
|
|
148
|
+
# prevent.
|
|
149
|
+
try:
|
|
150
|
+
_rename_in_manifest(project_root, old=old, new=new)
|
|
151
|
+
except (OSError, ValueError) as exc:
|
|
152
|
+
return RenameGroupResult(
|
|
153
|
+
plan=plan, backup_path=backup_path,
|
|
154
|
+
failure=RenameGroupFailure(reasons=[
|
|
155
|
+
f"records/record_group_fields/ext table renamed successfully, but updating "
|
|
156
|
+
f"{init_paths.PROPOSAL_FILENAME} failed: {exc} - fix it by hand for these "
|
|
157
|
+
f"paths: {plan.manifest_entry_paths}"
|
|
158
|
+
]),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return RenameGroupResult(plan=plan, backup_path=backup_path, failure=None)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Pure formatting of the SessionStart catch-up digest (§11). Takes structured
|
|
2
|
+
sweep results, returns a string. No I/O."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Outcome(str, Enum):
|
|
10
|
+
RAN = "ran"
|
|
11
|
+
FAILED = "failed"
|
|
12
|
+
LAUNCHED = "launched"
|
|
13
|
+
SUSPENDED = "suspended"
|
|
14
|
+
SUMMARY = "summary"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class JobReport:
|
|
19
|
+
job_id: str
|
|
20
|
+
outcome: Outcome
|
|
21
|
+
surface: bool
|
|
22
|
+
overdue: str
|
|
23
|
+
ran: int
|
|
24
|
+
deferred: int
|
|
25
|
+
expired: int
|
|
26
|
+
consecutive_failures: int
|
|
27
|
+
# Relative-age suffix (e.g. "12d ago"). Always set by the surfacing layer
|
|
28
|
+
# for FAILED/SUSPENDED reports, and for SUMMARY reports (oldest folded
|
|
29
|
+
# entry); unused otherwise.
|
|
30
|
+
age: str | None = None
|
|
31
|
+
# SUMMARY only: number of routine entries folded into this one line.
|
|
32
|
+
count: int = 0
|
|
33
|
+
# RAN only: a successful-but-nonzero-exit run's captured stdout (e.g. a
|
|
34
|
+
# drift monitor's "found something" report). Always surfaced with a
|
|
35
|
+
# warning wording, never folded into SUMMARY - see surface.py.
|
|
36
|
+
findings: str | None = None
|
|
37
|
+
# RAN only: a successful, zero-exit run's captured stdout (e.g. a verify
|
|
38
|
+
# command's "all OK" confirmation). Always surfaced with neutral wording,
|
|
39
|
+
# distinct from `findings`' warning wording - see surface.py. Mutually
|
|
40
|
+
# exclusive with `findings`: a given run is either a clean 0-exit or a
|
|
41
|
+
# findings-bearing nonzero exit, never both.
|
|
42
|
+
output: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _ordinal(n: int) -> str:
|
|
46
|
+
if 10 <= n % 100 <= 20:
|
|
47
|
+
suffix = "th"
|
|
48
|
+
else:
|
|
49
|
+
suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
|
|
50
|
+
return f"{n}{suffix}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _line(report: JobReport) -> str | None:
|
|
54
|
+
if report.outcome is Outcome.SUMMARY:
|
|
55
|
+
return (
|
|
56
|
+
f"⏱ {report.count} routine job runs since your last session, "
|
|
57
|
+
f"oldest {report.age}"
|
|
58
|
+
)
|
|
59
|
+
if report.outcome is Outcome.SUSPENDED:
|
|
60
|
+
age_suffix = f", {report.age}" if report.age else ""
|
|
61
|
+
return (
|
|
62
|
+
f"⛔ {report.job_id} auto-suspended after "
|
|
63
|
+
f"{report.consecutive_failures} consecutive failures{age_suffix} — see "
|
|
64
|
+
f"`ccsched status {report.job_id}` / run `ccsched enable {report.job_id}` after fixing"
|
|
65
|
+
)
|
|
66
|
+
if report.outcome is Outcome.FAILED:
|
|
67
|
+
age_suffix = f", {report.age}" if report.age else ""
|
|
68
|
+
return (
|
|
69
|
+
f"✗ {report.job_id} failed "
|
|
70
|
+
f"({_ordinal(report.consecutive_failures)} consecutive{age_suffix}) — see "
|
|
71
|
+
f"`ccsched status {report.job_id}`"
|
|
72
|
+
)
|
|
73
|
+
if report.outcome is Outcome.RAN:
|
|
74
|
+
# A completed run is always shown, regardless of `surface` - unlike
|
|
75
|
+
# LAUNCHED below, "it ran" is never something a job can silence.
|
|
76
|
+
overdue = f" ({report.overdue} overdue)" if report.overdue else ""
|
|
77
|
+
# Every RAN variant below carries this same age suffix (when set) so a
|
|
78
|
+
# result read back from an earlier run is never mistaken for something
|
|
79
|
+
# that just happened - e.g. sitting next to a fresh LAUNCHED line for
|
|
80
|
+
# the same job from this same sweep.
|
|
81
|
+
age_suffix = f", {report.age}" if report.age else ""
|
|
82
|
+
if report.findings:
|
|
83
|
+
# Bypasses the surface gate deliberately, same as FAILED/SUSPENDED
|
|
84
|
+
# above: findings are signal, not routine noise a job can silence.
|
|
85
|
+
header = f"⚠ {report.job_id} ran with findings{overdue}{age_suffix}:"
|
|
86
|
+
body = "\n".join(f" {line}" for line in report.findings.splitlines())
|
|
87
|
+
return f"{header}\n{body}"
|
|
88
|
+
if report.output:
|
|
89
|
+
# A clean (0-exit) run's captured stdout - neutral wording, kept
|
|
90
|
+
# distinct from the findings/warning case above so a passing check
|
|
91
|
+
# is never mistaken for one that found something. One line-group:
|
|
92
|
+
# never falls through to the bare "✓ ran" case below.
|
|
93
|
+
header = f"✓ ran {report.job_id}{overdue}{age_suffix}:"
|
|
94
|
+
body = "\n".join(f" {line}" for line in report.output.splitlines())
|
|
95
|
+
return f"{header}\n{body}"
|
|
96
|
+
if report.count > 1:
|
|
97
|
+
# Several unremarkable runs of the same job coalesced within one
|
|
98
|
+
# sweep (§ coalesce fix) - one line with a count, not N
|
|
99
|
+
# near-identical bare ticks. Only bare (no output/findings) runs
|
|
100
|
+
# are ever coalesced - see surface.py - so this never competes
|
|
101
|
+
# with the findings/output branches above.
|
|
102
|
+
recency = f" (most recently {report.age})" if report.age else ""
|
|
103
|
+
return f"✓ ran {report.job_id} ×{report.count}{recency}"
|
|
104
|
+
base = f"✓ ran {report.job_id}{overdue}{age_suffix}"
|
|
105
|
+
if report.deferred:
|
|
106
|
+
base += f"\n⏳ {report.job_id}: {report.deferred} backfills deferred"
|
|
107
|
+
if report.expired:
|
|
108
|
+
base += f"\n ({report.expired} missed run(s) dropped as expired)"
|
|
109
|
+
return base
|
|
110
|
+
# Only Outcome.LAUNCHED can reach here - SUMMARY/SUSPENDED/FAILED/RAN are
|
|
111
|
+
# all fully handled above and always return. A "started" notice is not
|
|
112
|
+
# "it ran", so it is the one outcome that still respects `surface`.
|
|
113
|
+
if not report.surface:
|
|
114
|
+
return None
|
|
115
|
+
if report.count > 1:
|
|
116
|
+
# Several launches of the same job coalesced within one sweep (§
|
|
117
|
+
# coalesce fix) - see the matching RAN branch above.
|
|
118
|
+
recency = f" (most recently {report.age})" if report.age else ""
|
|
119
|
+
return f"▶ launched {report.job_id} ×{report.count}{recency}"
|
|
120
|
+
age_suffix = f", {report.age}" if report.age else ""
|
|
121
|
+
return f"▶ launched {report.job_id} (running in background){age_suffix}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def format_digest(reports: list[JobReport], *, parse_error: str | None = None) -> str:
|
|
125
|
+
if parse_error is not None:
|
|
126
|
+
return f"[cc-scheduler] job registry failed to load — no jobs ran: {parse_error}"
|
|
127
|
+
lines = [line for line in (_line(r) for r in reports) if line is not None]
|
|
128
|
+
if not lines:
|
|
129
|
+
return ""
|
|
130
|
+
return "\n".join(["[cc-scheduler] scheduled-task catch-up:", *lines])
|
{cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/ledger.py
RENAMED
|
@@ -112,6 +112,24 @@ def read_since(offset: int) -> tuple[list[dict[str, object]], int]:
|
|
|
112
112
|
conn.close()
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
def offset_before_ts(cutoff_ts: str) -> int:
|
|
116
|
+
"""The highest catchup_events id whose ts is strictly earlier than
|
|
117
|
+
cutoff_ts (0 if none) - the offset such that read_since(this) yields
|
|
118
|
+
every row with ts >= cutoff_ts. Used by the SessionStart digest's 24h
|
|
119
|
+
rolling lookback (§9.3 widen fix) to compute a starting offset without
|
|
120
|
+
loading the whole ledger table into memory; idx_catchup_events_ts makes
|
|
121
|
+
this a single index range scan rather than a full-table filter."""
|
|
122
|
+
conn = telemetry_store.connect()
|
|
123
|
+
try:
|
|
124
|
+
row = conn.execute(
|
|
125
|
+
"SELECT COALESCE(MAX(id), 0) AS m FROM catchup_events WHERE ts < ?",
|
|
126
|
+
(cutoff_ts,),
|
|
127
|
+
).fetchone()
|
|
128
|
+
return int(row["m"])
|
|
129
|
+
finally:
|
|
130
|
+
conn.close()
|
|
131
|
+
|
|
132
|
+
|
|
115
133
|
def current_offset() -> int:
|
|
116
134
|
"""The current max catchup_events id (0 if empty). Used to seed a
|
|
117
135
|
brand-new session's cursor (§9.3) so its first digest reflects only
|