cctally 1.113.0 → 1.115.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 +47 -0
- package/README.md +4 -6
- package/bin/_cctally_account.py +32 -8
- package/bin/_cctally_cache.py +6035 -722
- package/bin/_cctally_completion.py +1287 -0
- package/bin/_cctally_config.py +37 -0
- package/bin/_cctally_core.py +400 -2
- package/bin/_cctally_dashboard.py +98 -20
- package/bin/_cctally_dashboard_conversation.py +292 -23
- package/bin/_cctally_dashboard_envelope.py +16 -3
- package/bin/_cctally_dashboard_perf.py +128 -9
- package/bin/_cctally_dashboard_sources.py +285 -213
- package/bin/_cctally_db.py +1177 -23
- package/bin/_cctally_diagnosis_sources.py +22 -4
- package/bin/_cctally_doctor.py +572 -45
- package/bin/_cctally_five_hour.py +84 -33
- package/bin/_cctally_forecast.py +108 -15
- package/bin/_cctally_journal.py +3950 -116
- package/bin/_cctally_journal_repair.py +101 -12
- package/bin/_cctally_milestone_history.py +82 -9
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_percent_breakdown.py +3 -2
- package/bin/_cctally_project.py +8 -2
- package/bin/_cctally_quarantine.py +652 -0
- package/bin/_cctally_quota.py +227 -37
- package/bin/_cctally_quota_model.py +360 -83
- package/bin/_cctally_record.py +691 -149
- package/bin/_cctally_rederive.py +320 -41
- package/bin/_cctally_refresh.py +84 -13
- package/bin/_cctally_source_analytics.py +83 -17
- package/bin/_cctally_statusline.py +6154 -517
- package/bin/_cctally_store.py +367 -53
- package/bin/_cctally_transcript.py +8 -7
- package/bin/_cctally_tui.py +61 -56
- package/bin/_cctally_weekrefs.py +128 -21
- package/bin/_lib_account_binding.py +152 -0
- package/bin/_lib_account_protocol.py +386 -0
- package/bin/_lib_aggregators.py +4 -0
- package/bin/_lib_cache_coverage.py +13 -1
- package/bin/_lib_cache_report.py +35 -6
- package/bin/_lib_cache_writer_lock.py +19 -3
- package/bin/_lib_clone_copy.py +142 -0
- package/bin/_lib_codex_conversation.py +32 -0
- package/bin/_lib_codex_conversation_query.py +297 -49
- package/bin/_lib_codex_conversation_watch.py +59 -14
- package/bin/_lib_codex_drain_admission.py +214 -0
- package/bin/_lib_codex_metadata.py +18 -1
- package/bin/_lib_codex_publication_barrier.py +277 -0
- package/bin/_lib_codex_rollout_container.py +502 -0
- package/bin/_lib_conversation_dispatch.py +3 -0
- package/bin/_lib_conversation_query.py +106 -59
- package/bin/_lib_conversation_retention.py +1605 -640
- package/bin/_lib_cost_provenance.py +53 -2
- package/bin/_lib_detector_access_registry.py +3817 -0
- package/bin/_lib_doctor.py +714 -69
- package/bin/_lib_five_hour.py +126 -0
- package/bin/_lib_fmt.py +11 -0
- package/bin/_lib_ingest_frontier.py +414 -20
- package/bin/_lib_journal.py +141 -10
- package/bin/_lib_jsonl.py +5 -4
- package/bin/_lib_legacy_spool.py +126 -0
- package/bin/_lib_legacy_writer_evidence.py +136 -0
- package/bin/_lib_merge_gate.py +231 -86
- package/bin/_lib_meter_rate_change.py +10 -0
- package/bin/_lib_outcome_quarantine.py +690 -0
- package/bin/_lib_perf.py +27 -1
- package/bin/_lib_pricing.py +199 -9
- package/bin/_lib_pricing_check.py +20 -4
- package/bin/_lib_quarantine.py +154 -0
- package/bin/_lib_quota_calibration.py +53 -8
- package/bin/_lib_quota_copy.py +2 -1
- package/bin/_lib_quota_model.py +288 -46
- package/bin/_lib_reclaim_planner.py +792 -0
- package/bin/_lib_rederive.py +319 -82
- package/bin/_lib_segment_summary.py +35 -2
- package/bin/_lib_snapshot_cache.py +21 -0
- package/bin/_lib_sqlite_close.py +81 -0
- package/bin/_lib_statusline_candidates.py +292 -23
- package/bin/_lib_subscription_weeks.py +100 -17
- package/bin/_lib_tick_stats.py +217 -17
- package/bin/_lib_wal_checkpoint.py +1077 -0
- package/bin/_lib_write_budget.py +483 -0
- package/bin/_lib_write_io.py +703 -0
- package/bin/cctally +31 -1
- package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
- package/dashboard/static/assets/index-BKITk1CT.css +1 -0
- package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +19 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
- package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
|
@@ -282,29 +282,32 @@ def _session_first_prompt_titles_map(conn, session_ids):
|
|
|
282
282
|
to this helper's result dict): once a sid resolves a first-prompt title, later
|
|
283
283
|
rows in its 12-row window do not overwrite it (Codex P1-3).
|
|
284
284
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
285
|
+
#901 Amendment 19 T1 (spec I1): one index-served ``LIMIT 12`` lookup per
|
|
286
|
+
session, walking ``idx_conv_session_ts`` in ``(timestamp_utc, id)`` order —
|
|
287
|
+
a true top-12 with no sorter. The ``ROW_NUMBER()`` window it replaces
|
|
288
|
+
ranked the session's WHOLE human partition through a temp b-tree before
|
|
289
|
+
keeping twelve, and one session's history is not a bound. Same rows in the
|
|
290
|
+
same order: sessions in ascending id order (the window's
|
|
291
|
+
``ORDER BY session_id``), each one's earliest twelve candidates by
|
|
292
|
+
``(timestamp_utc, id)`` with NULL timestamps first. Honors
|
|
293
|
+
``_reingest_pending(conn)``'s skip_skill_titles gate: while 005's reingest
|
|
294
|
+
is pending a stale ``human`` row may actually be an injected skill body, so
|
|
295
|
+
skip those as title candidates (gated on the flag so a genuine
|
|
296
|
+
post-reingest human prompt starting with the preamble stays a normal title
|
|
297
|
+
— Codex code-review P2)."""
|
|
292
298
|
if not session_ids:
|
|
293
299
|
return {}
|
|
294
300
|
titles = {}
|
|
295
301
|
skip_skill_titles = _reingest_pending(conn)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
") WHERE rn <= 12 ORDER BY session_id, rn",
|
|
306
|
-
tuple(session_ids),
|
|
307
|
-
).fetchall()
|
|
302
|
+
rows = []
|
|
303
|
+
for sid in sorted({s for s in session_ids if s is not None}, key=str):
|
|
304
|
+
rows.extend(conn.execute(
|
|
305
|
+
"SELECT session_id, text FROM conversation_messages "
|
|
306
|
+
"WHERE session_id = ? AND entry_type='human' "
|
|
307
|
+
" AND is_sidechain=0 AND COALESCE(text,'') <> '' "
|
|
308
|
+
"ORDER BY timestamp_utc, id LIMIT 12",
|
|
309
|
+
(sid,),
|
|
310
|
+
).fetchall())
|
|
308
311
|
for sid, text in rows:
|
|
309
312
|
if sid in titles:
|
|
310
313
|
continue # first-wins: already resolved a title
|
|
@@ -578,7 +581,7 @@ _CACHE_FAILURE_CACHE_FLOOR = 20_000 # prior cache must be meaningful to "lo
|
|
|
578
581
|
_CACHE_FAILURE_CREATE_FLOOR = 20_000 # the re-creation must be substantial / real cost
|
|
579
582
|
|
|
580
583
|
|
|
581
|
-
def _cache_failure_wasted_usd(model, lost, *, speed):
|
|
584
|
+
def _cache_failure_wasted_usd(model, lost, *, speed, prompt_tokens):
|
|
582
585
|
"""Marginal extra paid by re-creating `lost` previously-cached tokens at the
|
|
583
586
|
cache-WRITE rate instead of reading them at the cache-READ rate. Reuses the
|
|
584
587
|
pricing chokepoint `_calculate_entry_cost` (zero on unknown models — the
|
|
@@ -586,40 +589,54 @@ def _cache_failure_wasted_usd(model, lost, *, speed):
|
|
|
586
589
|
any cost-snapshot / budget / reconciled figure — a display-only estimate.
|
|
587
590
|
The lost-prefix subset has no authoritative mapping to the source row's
|
|
588
591
|
5m/1h write buckets, so this preserves the existing 5m estimate while
|
|
589
|
-
applying the retained effective speed tier.
|
|
592
|
+
applying the retained effective speed tier.
|
|
593
|
+
|
|
594
|
+
`prompt_tokens` (REQUIRED, #929) is the SOURCE request's real prompt total
|
|
595
|
+
(input + cache creation + cache read). Both synthetic calls pass it as
|
|
596
|
+
`prompt_tokens_for_tier`, so a prompt-length-priced card (Claude Haiku 5.5)
|
|
597
|
+
bills the subset at the card the real request selected, never at the card
|
|
598
|
+
the smaller subset would select on its own."""
|
|
590
599
|
write = _calculate_entry_cost(
|
|
591
600
|
model or "",
|
|
592
601
|
claude_usage_dict(
|
|
593
602
|
cache_1h_tokens=None, speed=speed, cache_creation_tokens=lost
|
|
594
603
|
),
|
|
604
|
+
prompt_tokens_for_tier=prompt_tokens,
|
|
595
605
|
)
|
|
596
606
|
read = _calculate_entry_cost(
|
|
597
607
|
model or "",
|
|
598
608
|
claude_usage_dict(
|
|
599
609
|
cache_1h_tokens=None, speed=speed, cache_read_tokens=lost
|
|
600
610
|
),
|
|
611
|
+
prompt_tokens_for_tier=prompt_tokens,
|
|
601
612
|
)
|
|
602
613
|
return write - read
|
|
603
614
|
|
|
604
615
|
|
|
605
|
-
def _cache_read_saved_usd(model, cache_read, *, speed):
|
|
616
|
+
def _cache_read_saved_usd(model, cache_read, *, speed, prompt_tokens):
|
|
606
617
|
"""Marginal USD the cache SAVED this turn: the `cache_read` prefix priced at
|
|
607
618
|
the full input rate minus its actual cache-READ rate. Display-only (same
|
|
608
619
|
caveat as `_cache_failure_wasted_usd`): NEVER summed into a cost-snapshot /
|
|
609
620
|
budget / reconciled figure. `input_tokens` and `cache_read_input_tokens` are
|
|
610
621
|
independent keys in the Claude `_calculate_entry_cost` (no subset
|
|
611
|
-
subtraction), so passing each alone yields the two rates cleanly.
|
|
622
|
+
subtraction), so passing each alone yields the two rates cleanly.
|
|
623
|
+
|
|
624
|
+
`prompt_tokens` (REQUIRED, #929) is the source request's real prompt total,
|
|
625
|
+
passed to both calls as `prompt_tokens_for_tier` exactly as in
|
|
626
|
+
`_cache_failure_wasted_usd`."""
|
|
612
627
|
full = _calculate_entry_cost(
|
|
613
628
|
model or "",
|
|
614
629
|
claude_usage_dict(
|
|
615
630
|
cache_1h_tokens=None, speed=speed, input_tokens=cache_read
|
|
616
631
|
),
|
|
632
|
+
prompt_tokens_for_tier=prompt_tokens,
|
|
617
633
|
)
|
|
618
634
|
read = _calculate_entry_cost(
|
|
619
635
|
model or "",
|
|
620
636
|
claude_usage_dict(
|
|
621
637
|
cache_1h_tokens=None, speed=speed, cache_read_tokens=cache_read
|
|
622
638
|
),
|
|
639
|
+
prompt_tokens_for_tier=prompt_tokens,
|
|
623
640
|
)
|
|
624
641
|
return full - read
|
|
625
642
|
|
|
@@ -749,11 +766,17 @@ def _stamp_cache_failures(items):
|
|
|
749
766
|
"""
|
|
750
767
|
events, sources = _cache_failure_events_from_items(items)
|
|
751
768
|
for idx, prev_cached, lost, model, speed in _iter_cache_failures(events):
|
|
769
|
+
# #929: the flagged request's REAL prompt selects the pricing card, not
|
|
770
|
+
# the lost-prefix subset being priced.
|
|
771
|
+
tok = sources[idx].get("tokens") or {}
|
|
772
|
+
prompt_tokens = ((tok.get("input", 0) or 0)
|
|
773
|
+
+ (tok.get("cache_creation", 0) or 0)
|
|
774
|
+
+ (tok.get("cache_read", 0) or 0))
|
|
752
775
|
sources[idx]["cache_failure"] = {
|
|
753
776
|
"tokens_recreated": lost,
|
|
754
777
|
"prev_cached": prev_cached,
|
|
755
778
|
"est_wasted_usd": _cache_failure_wasted_usd(
|
|
756
|
-
model, lost, speed=speed
|
|
779
|
+
model, lost, speed=speed, prompt_tokens=prompt_tokens
|
|
757
780
|
),
|
|
758
781
|
}
|
|
759
782
|
|
|
@@ -1037,18 +1060,24 @@ def _session_cost_map(conn, session_ids):
|
|
|
1037
1060
|
shared ``_turn_costs_for_keys`` helper (cost-once per deduped session_entries
|
|
1038
1061
|
row), summing into the owning session. (msg_id, req_id) is globally unique in
|
|
1039
1062
|
session_entries and maps to exactly one session_id, so per-session sums are
|
|
1040
|
-
clean and a turn replayed across files contributes once.
|
|
1063
|
+
clean and a turn replayed across files contributes once.
|
|
1064
|
+
|
|
1065
|
+
#901 Amendment 19 T1 (spec I1): the de-duplication runs in Python over the
|
|
1066
|
+
streamed rows, keeping each key's FIRST occurrence in scan order — exactly
|
|
1067
|
+
the rows, and the order, ``SELECT DISTINCT`` returned. The DISTINCT built a
|
|
1068
|
+
temp b-tree of every turn of every touched session, and one session's
|
|
1069
|
+
history is not a bound."""
|
|
1041
1070
|
costs = {sid: 0.0 for sid in session_ids}
|
|
1042
1071
|
if not session_ids:
|
|
1043
1072
|
return costs
|
|
1044
1073
|
placeholders = ",".join("?" for _ in session_ids)
|
|
1045
|
-
pairs = conn.execute(
|
|
1046
|
-
"SELECT
|
|
1074
|
+
pairs = list(dict.fromkeys(conn.execute(
|
|
1075
|
+
"SELECT session_id, msg_id, req_id "
|
|
1047
1076
|
"FROM conversation_messages "
|
|
1048
1077
|
"WHERE session_id IN (%s) AND msg_id IS NOT NULL AND req_id IS NOT NULL"
|
|
1049
1078
|
% placeholders,
|
|
1050
1079
|
list(session_ids),
|
|
1051
|
-
)
|
|
1080
|
+
)))
|
|
1052
1081
|
if not pairs:
|
|
1053
1082
|
return costs
|
|
1054
1083
|
key_cost = _turn_costs_for_keys(conn, [(m, r) for _, m, r in pairs])
|
|
@@ -1084,20 +1113,25 @@ def _models_main_first(rows):
|
|
|
1084
1113
|
|
|
1085
1114
|
def _session_models_map(conn, session_ids):
|
|
1086
1115
|
"""{session_id: models ordered MAIN-session-first (subagent/sidechain-only
|
|
1087
|
-
models last), alphabetical within each group — see _models_main_first}.
|
|
1116
|
+
models last), alphabetical within each group — see _models_main_first}.
|
|
1117
|
+
|
|
1118
|
+
#901 Amendment 19 T1 (spec I1): the de-duplication runs in Python over the
|
|
1119
|
+
streamed rows (one entry per distinct tuple), where a ``SELECT DISTINCT``
|
|
1120
|
+
built a temp b-tree over every touched session's whole history.
|
|
1121
|
+
``_models_main_first`` reads its input as sets, so the result is the same."""
|
|
1088
1122
|
out = {sid: [] for sid in session_ids}
|
|
1089
1123
|
if not session_ids:
|
|
1090
1124
|
return out
|
|
1091
1125
|
placeholders = ",".join("?" for _ in session_ids)
|
|
1092
1126
|
sql = (
|
|
1093
|
-
"SELECT
|
|
1127
|
+
"SELECT session_id, model, source_path, is_sidechain "
|
|
1094
1128
|
"FROM conversation_messages "
|
|
1095
1129
|
"WHERE session_id IN (%s) AND model IS NOT NULL AND model != ''"
|
|
1096
1130
|
% placeholders
|
|
1097
1131
|
)
|
|
1098
1132
|
per_session = {}
|
|
1099
|
-
for sid, model, source_path, is_sidechain in
|
|
1100
|
-
sql, list(session_ids)):
|
|
1133
|
+
for sid, model, source_path, is_sidechain in dict.fromkeys(
|
|
1134
|
+
conn.execute(sql, list(session_ids))):
|
|
1101
1135
|
per_session.setdefault(sid, []).append(
|
|
1102
1136
|
(model, source_path, is_sidechain))
|
|
1103
1137
|
for sid, rows in per_session.items():
|
|
@@ -1113,30 +1147,31 @@ def _session_latest_meta_map(conn, session_ids):
|
|
|
1113
1147
|
latest non-null git_branch may land on DIFFERENT rows (the newest row can
|
|
1114
1148
|
carry one but not the other), so each is resolved independently.
|
|
1115
1149
|
|
|
1116
|
-
#
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
id
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1150
|
+
#901 Q11 (spec §5.3a, 901-PA-001 b): two ``LIMIT 1`` lookups per session,
|
|
1151
|
+
one per column, each ``WHERE <col> IS NOT NULL ORDER BY timestamp_utc DESC,
|
|
1152
|
+
id DESC`` and served by its partial index on ``(session_id, timestamp_utc,
|
|
1153
|
+
id)`` (conversations migration 011): one seek, however long the session.
|
|
1154
|
+
The #217 S1 window query this replaces (two ``FIRST_VALUE`` windows over
|
|
1155
|
+
each touched session) sorted the session's whole history through temp
|
|
1156
|
+
b-trees on every conversation-sync pass, and one session is not a bound.
|
|
1157
|
+
The semantics are unchanged: an empty string is a value, NULL timestamps
|
|
1158
|
+
order below every real one, ties fall to the larger ``id``, an all-NULL
|
|
1159
|
+
column or a missing session gives ``None``. Guarded by
|
|
1160
|
+
``test_session_latest_meta_map_parity_*`` and
|
|
1161
|
+
``tests/test_901_session_latest_meta.py`` (a Python reference and the
|
|
1162
|
+
frozen window query, over sparse metadata in long histories)."""
|
|
1124
1163
|
meta = {sid: (None, None) for sid in session_ids}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
"FROM conversation_messages WHERE session_id IN (%s)" % placeholders
|
|
1137
|
-
)
|
|
1138
|
-
for sid, cwd, branch in conn.execute(sql, list(session_ids)):
|
|
1139
|
-
meta[sid] = (cwd, branch)
|
|
1164
|
+
for sid in meta:
|
|
1165
|
+
cwd = conn.execute(
|
|
1166
|
+
"SELECT cwd FROM conversation_messages "
|
|
1167
|
+
"WHERE session_id = ? AND cwd IS NOT NULL "
|
|
1168
|
+
"ORDER BY timestamp_utc DESC, id DESC LIMIT 1", (sid,)).fetchone()
|
|
1169
|
+
branch = conn.execute(
|
|
1170
|
+
"SELECT git_branch FROM conversation_messages "
|
|
1171
|
+
"WHERE session_id = ? AND git_branch IS NOT NULL "
|
|
1172
|
+
"ORDER BY timestamp_utc DESC, id DESC LIMIT 1", (sid,)).fetchone()
|
|
1173
|
+
meta[sid] = (cwd[0] if cwd is not None else None,
|
|
1174
|
+
branch[0] if branch is not None else None)
|
|
1140
1175
|
return meta
|
|
1141
1176
|
|
|
1142
1177
|
|
|
@@ -2982,8 +3017,12 @@ def get_conversation_outline(conn, session_id):
|
|
|
2982
3017
|
tokens[k] += tok.get(k, 0)
|
|
2983
3018
|
cr_tokens = tok.get("cache_read", 0) or 0
|
|
2984
3019
|
if cr_tokens > 0:
|
|
3020
|
+
# #929: the turn's real prompt selects the card.
|
|
2985
3021
|
cache_saved += _cache_read_saved_usd(
|
|
2986
|
-
it.get("model"), cr_tokens, speed=it.get("_speed")
|
|
3022
|
+
it.get("model"), cr_tokens, speed=it.get("_speed"),
|
|
3023
|
+
prompt_tokens=((tok.get("input", 0) or 0)
|
|
3024
|
+
+ (tok.get("cache_creation", 0) or 0)
|
|
3025
|
+
+ cr_tokens),
|
|
2987
3026
|
)
|
|
2988
3027
|
# Copy the cache-failure marker onto the OutlineTurn exactly where
|
|
2989
3028
|
# tokens is copied (assistant-only, rides the same source row) and
|
|
@@ -3256,11 +3295,19 @@ def _scoped_codex_anon_paths(conn, account_key: str):
|
|
|
3256
3295
|
raise RuntimeError("Codex raw threads table missing for scoped anonymization")
|
|
3257
3296
|
return set(), 0, 0
|
|
3258
3297
|
ambiguous = 0
|
|
3259
|
-
|
|
3298
|
+
# Threads can live in the last-good TEMP view, whose schema says nothing
|
|
3299
|
+
# about the physical accounting roots. Resolve roots independently and
|
|
3300
|
+
# bypass account-scope TEMP shadows; the provenance checks below still
|
|
3301
|
+
# decide which of those paths this account may use. A missing/unreadable
|
|
3302
|
+
# physical roots table propagates as a refusal, never a partial vocabulary.
|
|
3303
|
+
roots_table = (
|
|
3304
|
+
"main.codex_source_roots" if conn.execute(
|
|
3305
|
+
"SELECT 1 FROM main.sqlite_master WHERE type='table' "
|
|
3306
|
+
"AND name='codex_source_roots'").fetchone() is not None
|
|
3307
|
+
else "cache_db.codex_source_roots")
|
|
3260
3308
|
root_paths: dict[str, str] = {}
|
|
3261
3309
|
for root_key, root_path in conn.execute(
|
|
3262
|
-
"SELECT source_root_key, canonical_root_path FROM "
|
|
3263
|
-
+ thread_schema + ".codex_source_roots"
|
|
3310
|
+
"SELECT source_root_key, canonical_root_path FROM " + roots_table
|
|
3264
3311
|
):
|
|
3265
3312
|
if isinstance(root_key, str) and isinstance(root_path, str):
|
|
3266
3313
|
if root_path and os.path.isabs(root_path):
|