glaip-sdk 0.6.11__py3-none-any.whl → 0.6.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/__init__.py +42 -5
- {glaip_sdk-0.6.11.dist-info → glaip_sdk-0.6.14.dist-info}/METADATA +31 -37
- glaip_sdk-0.6.14.dist-info/RECORD +12 -0
- {glaip_sdk-0.6.11.dist-info → glaip_sdk-0.6.14.dist-info}/WHEEL +2 -1
- glaip_sdk-0.6.14.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.6.14.dist-info/top_level.txt +1 -0
- glaip_sdk/agents/__init__.py +0 -27
- glaip_sdk/agents/base.py +0 -1191
- glaip_sdk/cli/__init__.py +0 -9
- glaip_sdk/cli/account_store.py +0 -540
- glaip_sdk/cli/agent_config.py +0 -78
- glaip_sdk/cli/auth.py +0 -699
- glaip_sdk/cli/commands/__init__.py +0 -5
- glaip_sdk/cli/commands/accounts.py +0 -746
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/common_config.py +0 -101
- glaip_sdk/cli/commands/configure.py +0 -896
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/models.py +0 -69
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/commands/transcripts.py +0 -755
- glaip_sdk/cli/commands/update.py +0 -61
- glaip_sdk/cli/config.py +0 -95
- glaip_sdk/cli/constants.py +0 -38
- glaip_sdk/cli/context.py +0 -150
- glaip_sdk/cli/core/__init__.py +0 -79
- glaip_sdk/cli/core/context.py +0 -124
- glaip_sdk/cli/core/output.py +0 -846
- glaip_sdk/cli/core/prompting.py +0 -649
- glaip_sdk/cli/core/rendering.py +0 -187
- glaip_sdk/cli/display.py +0 -355
- glaip_sdk/cli/hints.py +0 -57
- glaip_sdk/cli/io.py +0 -112
- glaip_sdk/cli/main.py +0 -604
- glaip_sdk/cli/masking.py +0 -136
- glaip_sdk/cli/mcp_validators.py +0 -287
- glaip_sdk/cli/pager.py +0 -266
- glaip_sdk/cli/parsers/__init__.py +0 -7
- glaip_sdk/cli/parsers/json_input.py +0 -177
- glaip_sdk/cli/resolution.py +0 -67
- glaip_sdk/cli/rich_helpers.py +0 -27
- glaip_sdk/cli/slash/__init__.py +0 -15
- glaip_sdk/cli/slash/accounts_controller.py +0 -578
- glaip_sdk/cli/slash/accounts_shared.py +0 -75
- glaip_sdk/cli/slash/agent_session.py +0 -285
- glaip_sdk/cli/slash/prompt.py +0 -256
- glaip_sdk/cli/slash/remote_runs_controller.py +0 -566
- glaip_sdk/cli/slash/session.py +0 -1708
- glaip_sdk/cli/slash/tui/__init__.py +0 -9
- glaip_sdk/cli/slash/tui/accounts_app.py +0 -876
- glaip_sdk/cli/slash/tui/background_tasks.py +0 -72
- glaip_sdk/cli/slash/tui/loading.py +0 -58
- glaip_sdk/cli/slash/tui/remote_runs_app.py +0 -628
- glaip_sdk/cli/transcript/__init__.py +0 -31
- glaip_sdk/cli/transcript/cache.py +0 -536
- glaip_sdk/cli/transcript/capture.py +0 -329
- glaip_sdk/cli/transcript/export.py +0 -38
- glaip_sdk/cli/transcript/history.py +0 -815
- glaip_sdk/cli/transcript/launcher.py +0 -77
- glaip_sdk/cli/transcript/viewer.py +0 -374
- glaip_sdk/cli/update_notifier.py +0 -290
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk/cli/validators.py +0 -238
- glaip_sdk/client/__init__.py +0 -11
- glaip_sdk/client/_agent_payloads.py +0 -520
- glaip_sdk/client/agent_runs.py +0 -147
- glaip_sdk/client/agents.py +0 -1335
- glaip_sdk/client/base.py +0 -502
- glaip_sdk/client/main.py +0 -249
- glaip_sdk/client/mcps.py +0 -370
- glaip_sdk/client/run_rendering.py +0 -700
- glaip_sdk/client/shared.py +0 -21
- glaip_sdk/client/tools.py +0 -661
- glaip_sdk/client/validators.py +0 -198
- glaip_sdk/config/constants.py +0 -52
- glaip_sdk/mcps/__init__.py +0 -21
- glaip_sdk/mcps/base.py +0 -345
- glaip_sdk/models/__init__.py +0 -90
- glaip_sdk/models/agent.py +0 -47
- glaip_sdk/models/agent_runs.py +0 -116
- glaip_sdk/models/common.py +0 -42
- glaip_sdk/models/mcp.py +0 -33
- glaip_sdk/models/tool.py +0 -33
- glaip_sdk/payload_schemas/__init__.py +0 -7
- glaip_sdk/payload_schemas/agent.py +0 -85
- glaip_sdk/registry/__init__.py +0 -55
- glaip_sdk/registry/agent.py +0 -164
- glaip_sdk/registry/base.py +0 -139
- glaip_sdk/registry/mcp.py +0 -253
- glaip_sdk/registry/tool.py +0 -232
- glaip_sdk/runner/__init__.py +0 -59
- glaip_sdk/runner/base.py +0 -84
- glaip_sdk/runner/deps.py +0 -115
- glaip_sdk/runner/langgraph.py +0 -782
- glaip_sdk/runner/mcp_adapter/__init__.py +0 -13
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +0 -43
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +0 -257
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +0 -95
- glaip_sdk/runner/tool_adapter/__init__.py +0 -18
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +0 -44
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +0 -219
- glaip_sdk/tools/__init__.py +0 -22
- glaip_sdk/tools/base.py +0 -435
- glaip_sdk/utils/__init__.py +0 -86
- glaip_sdk/utils/a2a/__init__.py +0 -34
- glaip_sdk/utils/a2a/event_processor.py +0 -188
- glaip_sdk/utils/agent_config.py +0 -194
- glaip_sdk/utils/bundler.py +0 -267
- glaip_sdk/utils/client.py +0 -111
- glaip_sdk/utils/client_utils.py +0 -486
- glaip_sdk/utils/datetime_helpers.py +0 -58
- glaip_sdk/utils/discovery.py +0 -78
- glaip_sdk/utils/display.py +0 -135
- glaip_sdk/utils/export.py +0 -143
- glaip_sdk/utils/general.py +0 -61
- glaip_sdk/utils/import_export.py +0 -168
- glaip_sdk/utils/import_resolver.py +0 -492
- glaip_sdk/utils/instructions.py +0 -101
- glaip_sdk/utils/rendering/__init__.py +0 -115
- glaip_sdk/utils/rendering/formatting.py +0 -264
- glaip_sdk/utils/rendering/layout/__init__.py +0 -64
- glaip_sdk/utils/rendering/layout/panels.py +0 -156
- glaip_sdk/utils/rendering/layout/progress.py +0 -202
- glaip_sdk/utils/rendering/layout/summary.py +0 -74
- glaip_sdk/utils/rendering/layout/transcript.py +0 -606
- glaip_sdk/utils/rendering/models.py +0 -85
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -55
- glaip_sdk/utils/rendering/renderer/base.py +0 -1024
- glaip_sdk/utils/rendering/renderer/config.py +0 -27
- glaip_sdk/utils/rendering/renderer/console.py +0 -55
- glaip_sdk/utils/rendering/renderer/debug.py +0 -178
- glaip_sdk/utils/rendering/renderer/factory.py +0 -138
- glaip_sdk/utils/rendering/renderer/stream.py +0 -202
- glaip_sdk/utils/rendering/renderer/summary_window.py +0 -79
- glaip_sdk/utils/rendering/renderer/thinking.py +0 -273
- glaip_sdk/utils/rendering/renderer/toggle.py +0 -182
- glaip_sdk/utils/rendering/renderer/tool_panels.py +0 -442
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +0 -162
- glaip_sdk/utils/rendering/state.py +0 -204
- glaip_sdk/utils/rendering/step_tree_state.py +0 -100
- glaip_sdk/utils/rendering/steps/__init__.py +0 -34
- glaip_sdk/utils/rendering/steps/event_processor.py +0 -778
- glaip_sdk/utils/rendering/steps/format.py +0 -176
- glaip_sdk/utils/rendering/steps/manager.py +0 -387
- glaip_sdk/utils/rendering/timing.py +0 -36
- glaip_sdk/utils/rendering/viewer/__init__.py +0 -21
- glaip_sdk/utils/rendering/viewer/presenter.py +0 -184
- glaip_sdk/utils/resource_refs.py +0 -195
- glaip_sdk/utils/run_renderer.py +0 -41
- glaip_sdk/utils/runtime_config.py +0 -425
- glaip_sdk/utils/serialization.py +0 -424
- glaip_sdk/utils/sync.py +0 -142
- glaip_sdk/utils/tool_detection.py +0 -33
- glaip_sdk/utils/validation.py +0 -264
- glaip_sdk-0.6.11.dist-info/RECORD +0 -159
- glaip_sdk-0.6.11.dist-info/entry_points.txt +0 -3
|
@@ -1,815 +0,0 @@
|
|
|
1
|
-
"""Utilities for inspecting and cleaning cached agent run transcripts.
|
|
2
|
-
|
|
3
|
-
Authors:
|
|
4
|
-
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from collections.abc import Sequence
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from datetime import datetime, timezone
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
from glaip_sdk.cli.config import load_config
|
|
16
|
-
from glaip_sdk.cli.transcript.cache import ( # Reuse helpers even if marked private
|
|
17
|
-
MANIFEST_FILENAME,
|
|
18
|
-
_compute_duration_seconds, # type: ignore[attr-defined]
|
|
19
|
-
_timestamp_to_iso, # type: ignore[attr-defined]
|
|
20
|
-
ensure_cache_dir,
|
|
21
|
-
load_manifest_entries,
|
|
22
|
-
manifest_path,
|
|
23
|
-
transcript_path_candidates,
|
|
24
|
-
write_manifest,
|
|
25
|
-
)
|
|
26
|
-
from glaip_sdk.cli.utils import parse_json_line
|
|
27
|
-
from glaip_sdk.utils.datetime_helpers import coerce_datetime
|
|
28
|
-
|
|
29
|
-
DEFAULT_HISTORY_LIMIT = 10
|
|
30
|
-
MAX_HISTORY_LIMIT = 200
|
|
31
|
-
LEGACY_MANIFEST_KEYS: tuple[str, ...] = ("created_at", "source", "server_run_id")
|
|
32
|
-
UTC_MIN = datetime.min.replace(tzinfo=timezone.utc)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def coerce_sortable_datetime(value: datetime | None) -> datetime:
|
|
36
|
-
"""Return a timezone-aware datetime for sorting, using UTC minimum as fallback."""
|
|
37
|
-
if value is None:
|
|
38
|
-
return UTC_MIN
|
|
39
|
-
if value.tzinfo is None:
|
|
40
|
-
return value.replace(tzinfo=timezone.utc)
|
|
41
|
-
return value
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _safe_resolve(path: Path) -> Path:
|
|
45
|
-
"""Resolve a path while tolerating filesystem errors."""
|
|
46
|
-
try:
|
|
47
|
-
return path.resolve()
|
|
48
|
-
except OSError:
|
|
49
|
-
return path
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass(slots=True)
|
|
53
|
-
class HistoryEntry:
|
|
54
|
-
"""Normalised entry describing a cached transcript run."""
|
|
55
|
-
|
|
56
|
-
run_id: str
|
|
57
|
-
agent_name: str | None
|
|
58
|
-
agent_id: str | None
|
|
59
|
-
api_url: str | None
|
|
60
|
-
started_at: datetime | None
|
|
61
|
-
started_at_iso: str | None
|
|
62
|
-
finished_at: datetime | None
|
|
63
|
-
finished_at_iso: str | None
|
|
64
|
-
duration_seconds: int | None
|
|
65
|
-
size_bytes: int | None
|
|
66
|
-
filename: str | None
|
|
67
|
-
status: str
|
|
68
|
-
warning: str | None
|
|
69
|
-
migration_notice: str | None
|
|
70
|
-
is_current_session: bool
|
|
71
|
-
expected_path: Path | None
|
|
72
|
-
resolved_path: Path | None
|
|
73
|
-
manifest: dict[str, Any]
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@dataclass(slots=True)
|
|
77
|
-
class NormalizedEntry:
|
|
78
|
-
"""Internal representation of a manifest row post normalisation."""
|
|
79
|
-
|
|
80
|
-
persisted: dict[str, Any]
|
|
81
|
-
history: HistoryEntry
|
|
82
|
-
changed: bool
|
|
83
|
-
warnings: list[str]
|
|
84
|
-
resolved_path: Path | None
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@dataclass(slots=True)
|
|
88
|
-
class TimelineInfo:
|
|
89
|
-
"""Computed timestamp snapshot for a manifest entry."""
|
|
90
|
-
|
|
91
|
-
started_iso: str | None
|
|
92
|
-
finished_iso: str | None
|
|
93
|
-
started_source: Any
|
|
94
|
-
finished_source: Any
|
|
95
|
-
changed: bool
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@dataclass(slots=True)
|
|
99
|
-
class HistorySnapshot:
|
|
100
|
-
"""Collection of history entries and aggregate stats for presentation."""
|
|
101
|
-
|
|
102
|
-
manifest_path: Path
|
|
103
|
-
entries: list[HistoryEntry]
|
|
104
|
-
total_entries: int
|
|
105
|
-
cached_entries: int
|
|
106
|
-
total_size_bytes: int
|
|
107
|
-
index: dict[str, HistoryEntry]
|
|
108
|
-
warnings: list[str]
|
|
109
|
-
migration_summary: str | None
|
|
110
|
-
limit_requested: int
|
|
111
|
-
limit_applied: int
|
|
112
|
-
limit_clamped: bool
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@dataclass(slots=True)
|
|
116
|
-
class ClearResult:
|
|
117
|
-
"""Result of clearing cached transcripts from disk and manifest."""
|
|
118
|
-
|
|
119
|
-
manifest_path: Path
|
|
120
|
-
removed_entries: list[HistoryEntry]
|
|
121
|
-
not_found: list[str]
|
|
122
|
-
warnings: list[str]
|
|
123
|
-
reclaimed_bytes: int
|
|
124
|
-
cache_empty: bool
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _dedupe_run_id(run_id: str, existing: set[str]) -> str:
|
|
128
|
-
"""Ensure run identifiers remain unique when synthesising orphan entries."""
|
|
129
|
-
candidate = run_id or "run"
|
|
130
|
-
if candidate not in existing:
|
|
131
|
-
existing.add(candidate)
|
|
132
|
-
return candidate
|
|
133
|
-
|
|
134
|
-
base = candidate
|
|
135
|
-
counter = 2
|
|
136
|
-
while True:
|
|
137
|
-
candidate = f"{base}-{counter}"
|
|
138
|
-
if candidate not in existing:
|
|
139
|
-
existing.add(candidate)
|
|
140
|
-
return candidate
|
|
141
|
-
counter += 1
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _load_transcript_meta(path: Path) -> dict[str, Any] | None:
|
|
145
|
-
"""Read the metadata header from a cached transcript file."""
|
|
146
|
-
try:
|
|
147
|
-
with path.open("r", encoding="utf-8") as fh:
|
|
148
|
-
line = fh.readline()
|
|
149
|
-
except FileNotFoundError:
|
|
150
|
-
return None
|
|
151
|
-
except OSError:
|
|
152
|
-
return None
|
|
153
|
-
|
|
154
|
-
payload = parse_json_line(line)
|
|
155
|
-
if payload and payload.get("type") == "meta":
|
|
156
|
-
return payload
|
|
157
|
-
return None
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _to_int(value: Any) -> int | None:
|
|
161
|
-
"""Safely coerce numeric-like values to integers."""
|
|
162
|
-
try:
|
|
163
|
-
if value is None:
|
|
164
|
-
return None
|
|
165
|
-
return int(value)
|
|
166
|
-
except Exception:
|
|
167
|
-
return None
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def _resolve_cached_paths(entry: dict[str, Any], directory: Path) -> tuple[Path | None, Path | None]:
|
|
171
|
-
"""Return (resolved, expected) transcript paths for a manifest entry."""
|
|
172
|
-
candidates = transcript_path_candidates(entry, directory)
|
|
173
|
-
resolved = next((path for path in candidates if path.exists()), None)
|
|
174
|
-
expected = candidates[0] if candidates else None
|
|
175
|
-
return resolved, expected
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _ensure_filename_field(entry: dict[str, Any], resolved: Path | None, expected: Path | None) -> bool:
|
|
179
|
-
"""Ensure manifest rows include a filename pointing at the cached transcript."""
|
|
180
|
-
filename = entry.get("filename")
|
|
181
|
-
target = filename
|
|
182
|
-
if resolved is not None:
|
|
183
|
-
target = resolved.name
|
|
184
|
-
elif expected is not None:
|
|
185
|
-
target = expected.name
|
|
186
|
-
if target and target != filename:
|
|
187
|
-
entry["filename"] = target
|
|
188
|
-
return True
|
|
189
|
-
return False
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _normalise_cache_path(entry: dict[str, Any], *, directory: Path, resolved: Path | None) -> bool:
|
|
193
|
-
"""Ensure cache_path points to fallback locations outside the active cache directory."""
|
|
194
|
-
cache_path = entry.get("cache_path")
|
|
195
|
-
changed = False
|
|
196
|
-
|
|
197
|
-
target_path: Path | None = resolved
|
|
198
|
-
if target_path is None and cache_path:
|
|
199
|
-
target_path = Path(str(cache_path)).expanduser()
|
|
200
|
-
|
|
201
|
-
if target_path is None:
|
|
202
|
-
if cache_path is not None:
|
|
203
|
-
entry.pop("cache_path", None)
|
|
204
|
-
return True
|
|
205
|
-
return False
|
|
206
|
-
|
|
207
|
-
directory_root = _safe_resolve(directory)
|
|
208
|
-
target_root = _safe_resolve(target_path.parent)
|
|
209
|
-
|
|
210
|
-
should_keep_cache_path = True
|
|
211
|
-
try:
|
|
212
|
-
should_keep_cache_path = not directory_root.samefile(target_root)
|
|
213
|
-
except (OSError, AttributeError):
|
|
214
|
-
should_keep_cache_path = directory_root != target_root
|
|
215
|
-
|
|
216
|
-
if should_keep_cache_path:
|
|
217
|
-
cache_value = str(target_path)
|
|
218
|
-
if cache_value != cache_path:
|
|
219
|
-
entry["cache_path"] = cache_value
|
|
220
|
-
changed = True
|
|
221
|
-
elif cache_path is not None:
|
|
222
|
-
entry.pop("cache_path", None)
|
|
223
|
-
changed = True
|
|
224
|
-
|
|
225
|
-
if not entry.get("filename"):
|
|
226
|
-
entry["filename"] = target_path.name
|
|
227
|
-
changed = True
|
|
228
|
-
|
|
229
|
-
return changed
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _prune_legacy_fields(entry: dict[str, Any]) -> bool:
|
|
233
|
-
"""Remove legacy manifest keys after migrations complete."""
|
|
234
|
-
removed = False
|
|
235
|
-
for key in LEGACY_MANIFEST_KEYS:
|
|
236
|
-
if entry.pop(key, None) is not None:
|
|
237
|
-
removed = True
|
|
238
|
-
return removed
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def _select_meta_value(entry: dict[str, Any], meta: dict[str, Any] | None, key: str) -> Any:
|
|
242
|
-
"""Return the preferred value for a manifest key from meta or entry data."""
|
|
243
|
-
if meta and meta.get(key) not in (None, ""):
|
|
244
|
-
return meta[key]
|
|
245
|
-
return entry.get(key)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def _normalise_timestamps(entry: dict[str, Any], meta: dict[str, Any] | None) -> TimelineInfo:
|
|
249
|
-
"""Normalise started/finished timestamps and return a timeline snapshot."""
|
|
250
|
-
changed = False
|
|
251
|
-
|
|
252
|
-
started_source = _select_meta_value(entry, meta, "started_at") or entry.get("created_at")
|
|
253
|
-
started_iso = _timestamp_to_iso(started_source) if started_source is not None else None
|
|
254
|
-
if started_iso is None:
|
|
255
|
-
created_fallback = entry.get("created_at")
|
|
256
|
-
started_iso = _timestamp_to_iso(created_fallback)
|
|
257
|
-
if started_iso is None and isinstance(created_fallback, str):
|
|
258
|
-
started_iso = created_fallback
|
|
259
|
-
if started_iso and started_iso != entry.get("started_at"):
|
|
260
|
-
entry["started_at"] = started_iso
|
|
261
|
-
changed = True
|
|
262
|
-
|
|
263
|
-
finished_source = _select_meta_value(entry, meta, "finished_at")
|
|
264
|
-
finished_iso = _timestamp_to_iso(finished_source) if finished_source is not None else None
|
|
265
|
-
if finished_iso and finished_iso != entry.get("finished_at"):
|
|
266
|
-
entry["finished_at"] = finished_iso
|
|
267
|
-
changed = True
|
|
268
|
-
|
|
269
|
-
return TimelineInfo(
|
|
270
|
-
started_iso=entry.get("started_at"),
|
|
271
|
-
finished_iso=entry.get("finished_at"),
|
|
272
|
-
started_source=started_source if started_source is not None else entry.get("started_at"),
|
|
273
|
-
finished_source=finished_source if finished_source is not None else entry.get("finished_at"),
|
|
274
|
-
changed=changed,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def _ensure_duration(entry: dict[str, Any], timeline: TimelineInfo) -> bool:
|
|
279
|
-
"""Populate duration_seconds when both timestamps are available."""
|
|
280
|
-
duration = entry.get("duration_seconds")
|
|
281
|
-
if duration not in (None, "", 0):
|
|
282
|
-
return False
|
|
283
|
-
computed = _compute_duration_seconds(timeline.started_source, timeline.finished_source)
|
|
284
|
-
if computed is None:
|
|
285
|
-
return False
|
|
286
|
-
entry["duration_seconds"] = computed
|
|
287
|
-
return True
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def _merge_meta_fields(entry: dict[str, Any], meta: dict[str, Any] | None, keys: tuple[str, ...]) -> bool:
|
|
291
|
-
"""Overlay metadata attributes from the transcript header."""
|
|
292
|
-
if not meta:
|
|
293
|
-
return False
|
|
294
|
-
changed = False
|
|
295
|
-
for key in keys:
|
|
296
|
-
value = meta.get(key)
|
|
297
|
-
if value is None:
|
|
298
|
-
continue
|
|
299
|
-
if entry.get(key) != value:
|
|
300
|
-
entry[key] = value
|
|
301
|
-
changed = True
|
|
302
|
-
return changed
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def _resolve_size(entry: dict[str, Any], resolved: Path | None) -> tuple[int, bool]:
|
|
306
|
-
"""Return (size_bytes, changed) for a manifest entry."""
|
|
307
|
-
try:
|
|
308
|
-
size_bytes = resolved.stat().st_size if resolved else _to_int(entry.get("size_bytes")) or 0
|
|
309
|
-
except OSError:
|
|
310
|
-
size_bytes = _to_int(entry.get("size_bytes")) or 0
|
|
311
|
-
|
|
312
|
-
current = _to_int(entry.get("size_bytes"))
|
|
313
|
-
return size_bytes, current != size_bytes
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def _resolve_entry_warning(
|
|
317
|
-
entry: dict[str, Any], resolved: Path | None, expected: Path | None
|
|
318
|
-
) -> tuple[str | None, list[str]]:
|
|
319
|
-
"""Determine warning code and text for manifest anomalies."""
|
|
320
|
-
if resolved is not None:
|
|
321
|
-
return None, []
|
|
322
|
-
warnings = []
|
|
323
|
-
if expected is not None:
|
|
324
|
-
run_label = entry.get("run_id") or "?"
|
|
325
|
-
hint = entry.get("run_id") or "<RUN_ID>"
|
|
326
|
-
warnings.append(
|
|
327
|
-
f"Transcript file missing for run {run_label} (expected {expected}). "
|
|
328
|
-
f"Run `aip transcripts clear --id {hint}` or `aip transcripts clear --all` to remove stale entries."
|
|
329
|
-
)
|
|
330
|
-
return "transcript_missing", warnings
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
def _build_history_entry(
|
|
334
|
-
entry: dict[str, Any],
|
|
335
|
-
*,
|
|
336
|
-
resolved: Path | None,
|
|
337
|
-
expected: Path | None,
|
|
338
|
-
warning: str | None,
|
|
339
|
-
current_run_id: str | None,
|
|
340
|
-
timeline: TimelineInfo,
|
|
341
|
-
) -> HistoryEntry:
|
|
342
|
-
"""Create a HistoryEntry from normalised manifest data."""
|
|
343
|
-
started_iso = timeline.started_iso
|
|
344
|
-
finished_iso = timeline.finished_iso
|
|
345
|
-
return HistoryEntry(
|
|
346
|
-
run_id=str(entry.get("run_id") or ""),
|
|
347
|
-
agent_name=entry.get("agent_name"),
|
|
348
|
-
agent_id=entry.get("agent_id"),
|
|
349
|
-
api_url=entry.get("api_url"),
|
|
350
|
-
started_at=coerce_datetime(started_iso),
|
|
351
|
-
started_at_iso=str(started_iso) if started_iso is not None else None,
|
|
352
|
-
finished_at=coerce_datetime(finished_iso),
|
|
353
|
-
finished_at_iso=str(finished_iso) if finished_iso is not None else None,
|
|
354
|
-
duration_seconds=_to_int(entry.get("duration_seconds")),
|
|
355
|
-
size_bytes=_to_int(entry.get("size_bytes")),
|
|
356
|
-
filename=entry.get("filename"),
|
|
357
|
-
status="cached" if resolved is not None else "missing",
|
|
358
|
-
warning=warning,
|
|
359
|
-
migration_notice=entry.get("migration_notice"),
|
|
360
|
-
is_current_session=bool(current_run_id and history_run_id_eq(entry, current_run_id)),
|
|
361
|
-
expected_path=expected,
|
|
362
|
-
resolved_path=resolved,
|
|
363
|
-
manifest=entry,
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def _normalise_entry(
|
|
368
|
-
entry: dict[str, Any],
|
|
369
|
-
*,
|
|
370
|
-
directory: Path,
|
|
371
|
-
current_run_id: str | None,
|
|
372
|
-
) -> NormalizedEntry:
|
|
373
|
-
"""Normalise an existing manifest entry against on-disk metadata."""
|
|
374
|
-
persisted = dict(entry)
|
|
375
|
-
warnings: list[str] = []
|
|
376
|
-
changed = False
|
|
377
|
-
|
|
378
|
-
resolved_path, expected_path = _resolve_cached_paths(persisted, directory)
|
|
379
|
-
if _ensure_filename_field(persisted, resolved_path, expected_path):
|
|
380
|
-
changed = True
|
|
381
|
-
if _normalise_cache_path(persisted, directory=directory, resolved=resolved_path):
|
|
382
|
-
changed = True
|
|
383
|
-
|
|
384
|
-
meta = _load_transcript_meta(resolved_path) if resolved_path else None
|
|
385
|
-
|
|
386
|
-
timeline = _normalise_timestamps(persisted, meta)
|
|
387
|
-
if timeline.changed:
|
|
388
|
-
changed = True
|
|
389
|
-
if _ensure_duration(persisted, timeline):
|
|
390
|
-
changed = True
|
|
391
|
-
if _merge_meta_fields(persisted, meta, ("agent_id", "agent_name", "model")):
|
|
392
|
-
changed = True
|
|
393
|
-
|
|
394
|
-
size_bytes, size_changed = _resolve_size(persisted, resolved_path)
|
|
395
|
-
if size_changed:
|
|
396
|
-
persisted["size_bytes"] = size_bytes
|
|
397
|
-
changed = True
|
|
398
|
-
|
|
399
|
-
if "retained" in persisted:
|
|
400
|
-
retained = bool(persisted.get("retained", True))
|
|
401
|
-
if retained != persisted.get("retained"):
|
|
402
|
-
persisted["retained"] = retained
|
|
403
|
-
changed = True
|
|
404
|
-
else:
|
|
405
|
-
persisted["retained"] = True
|
|
406
|
-
|
|
407
|
-
warning, warning_messages = _resolve_entry_warning(persisted, resolved_path, expected_path)
|
|
408
|
-
warnings.extend(warning_messages)
|
|
409
|
-
|
|
410
|
-
if _prune_legacy_fields(persisted):
|
|
411
|
-
changed = True
|
|
412
|
-
|
|
413
|
-
history = _build_history_entry(
|
|
414
|
-
persisted,
|
|
415
|
-
resolved=resolved_path,
|
|
416
|
-
expected=expected_path,
|
|
417
|
-
warning=warning,
|
|
418
|
-
current_run_id=current_run_id,
|
|
419
|
-
timeline=timeline,
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
return NormalizedEntry(
|
|
423
|
-
persisted=persisted,
|
|
424
|
-
history=history,
|
|
425
|
-
changed=changed,
|
|
426
|
-
warnings=warnings,
|
|
427
|
-
resolved_path=resolved_path,
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def history_run_id_eq(entry: dict[str, Any], target_run_id: str) -> bool:
|
|
432
|
-
"""Return True when the manifest entry represents the target run id."""
|
|
433
|
-
run_id = entry.get("run_id")
|
|
434
|
-
return bool(run_id) and str(run_id) == str(target_run_id)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def _build_orphan_entry(
|
|
438
|
-
path: Path,
|
|
439
|
-
*,
|
|
440
|
-
existing_ids: set[str],
|
|
441
|
-
current_run_id: str | None,
|
|
442
|
-
) -> NormalizedEntry:
|
|
443
|
-
"""Create a synthetic manifest entry for an orphaned transcript file."""
|
|
444
|
-
meta = _load_transcript_meta(path)
|
|
445
|
-
run_id = None
|
|
446
|
-
if meta and meta.get("run_id"):
|
|
447
|
-
run_id = str(meta.get("run_id"))
|
|
448
|
-
if not run_id:
|
|
449
|
-
stem = path.stem
|
|
450
|
-
run_id = stem.replace("run-", "", 1) if stem.startswith("run-") else stem
|
|
451
|
-
run_id = _dedupe_run_id(run_id, existing_ids)
|
|
452
|
-
|
|
453
|
-
try:
|
|
454
|
-
size_bytes = path.stat().st_size
|
|
455
|
-
except OSError:
|
|
456
|
-
size_bytes = 0
|
|
457
|
-
|
|
458
|
-
persisted = {
|
|
459
|
-
"run_id": run_id,
|
|
460
|
-
"agent_id": meta.get("agent_id") if meta else None,
|
|
461
|
-
"agent_name": meta.get("agent_name") if meta else None,
|
|
462
|
-
"model": meta.get("model") if meta else None,
|
|
463
|
-
"started_at": meta.get("started_at"),
|
|
464
|
-
"finished_at": meta.get("finished_at"),
|
|
465
|
-
"duration_seconds": meta.get("duration_seconds"),
|
|
466
|
-
"size_bytes": size_bytes,
|
|
467
|
-
"filename": path.name,
|
|
468
|
-
"retained": True,
|
|
469
|
-
"migration_notice": "orphaned_transcript",
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
timeline = _normalise_timestamps(persisted, meta)
|
|
473
|
-
_ensure_duration(persisted, timeline)
|
|
474
|
-
_merge_meta_fields(persisted, meta, ("agent_id", "agent_name", "model"))
|
|
475
|
-
|
|
476
|
-
history = _build_history_entry(
|
|
477
|
-
persisted,
|
|
478
|
-
resolved=path,
|
|
479
|
-
expected=path,
|
|
480
|
-
warning=None,
|
|
481
|
-
current_run_id=current_run_id,
|
|
482
|
-
timeline=timeline,
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
return NormalizedEntry(
|
|
486
|
-
persisted=persisted,
|
|
487
|
-
history=history,
|
|
488
|
-
changed=True,
|
|
489
|
-
warnings=[],
|
|
490
|
-
resolved_path=path,
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
def _format_migration_summary(counters: dict[str, int]) -> str | None:
|
|
495
|
-
"""Summarise any cache migrations performed during snapshot normalisation."""
|
|
496
|
-
parts: list[str] = []
|
|
497
|
-
legacy = counters.get("legacy", 0)
|
|
498
|
-
if legacy:
|
|
499
|
-
parts.append(f"Migrated {legacy} legacy entries")
|
|
500
|
-
orphans = counters.get("orphans", 0)
|
|
501
|
-
if orphans:
|
|
502
|
-
parts.append(f"{orphans} orphan files added from disk")
|
|
503
|
-
missing = counters.get("missing", 0)
|
|
504
|
-
if missing:
|
|
505
|
-
parts.append(f"{missing} stale rows flagged as missing")
|
|
506
|
-
if not parts:
|
|
507
|
-
return None
|
|
508
|
-
return "; ".join(parts) + "."
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
def _process_manifest_entries(
|
|
512
|
-
raw_entries: list[dict[str, Any]],
|
|
513
|
-
*,
|
|
514
|
-
directory: Path,
|
|
515
|
-
current_run_id: str | None,
|
|
516
|
-
) -> tuple[
|
|
517
|
-
list[NormalizedEntry],
|
|
518
|
-
list[dict[str, Any]],
|
|
519
|
-
list[str],
|
|
520
|
-
dict[str, int],
|
|
521
|
-
bool,
|
|
522
|
-
int,
|
|
523
|
-
set[str],
|
|
524
|
-
]:
|
|
525
|
-
"""Normalise persisted manifest entries and collect aggregate stats."""
|
|
526
|
-
normalized_entries: list[NormalizedEntry] = []
|
|
527
|
-
persisted_entries: list[dict[str, Any]] = []
|
|
528
|
-
warnings: list[str] = []
|
|
529
|
-
counters = {"legacy": 0, "missing": 0, "cached": 0}
|
|
530
|
-
changed = False
|
|
531
|
-
total_bytes = 0
|
|
532
|
-
existing_ids: set[str] = set()
|
|
533
|
-
|
|
534
|
-
for entry in raw_entries:
|
|
535
|
-
normalized = _normalise_entry(entry, directory=directory, current_run_id=current_run_id)
|
|
536
|
-
normalized_entries.append(normalized)
|
|
537
|
-
persisted_entries.append(normalized.persisted)
|
|
538
|
-
warnings.extend(normalized.warnings)
|
|
539
|
-
if normalized.changed:
|
|
540
|
-
counters["legacy"] += 1
|
|
541
|
-
changed = True
|
|
542
|
-
if normalized.history.warning == "transcript_missing":
|
|
543
|
-
counters["missing"] += 1
|
|
544
|
-
if normalized.history.status == "cached":
|
|
545
|
-
counters["cached"] += 1
|
|
546
|
-
if normalized.history.size_bytes:
|
|
547
|
-
total_bytes += int(normalized.history.size_bytes or 0)
|
|
548
|
-
run_id = normalized.persisted.get("run_id")
|
|
549
|
-
if run_id:
|
|
550
|
-
existing_ids.add(str(run_id))
|
|
551
|
-
|
|
552
|
-
return normalized_entries, persisted_entries, warnings, counters, changed, total_bytes, existing_ids
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
def _append_orphan_entries(
|
|
556
|
-
directory: Path,
|
|
557
|
-
*,
|
|
558
|
-
current_run_id: str | None,
|
|
559
|
-
existing_ids: set[str],
|
|
560
|
-
normalized_entries: list[NormalizedEntry],
|
|
561
|
-
persisted_entries: list[dict[str, Any]],
|
|
562
|
-
counters: dict[str, int],
|
|
563
|
-
total_bytes: int,
|
|
564
|
-
) -> tuple[bool, int]:
|
|
565
|
-
"""Add on-disk transcripts that are missing from the manifest."""
|
|
566
|
-
resolved_paths = {entry.resolved_path.resolve() for entry in normalized_entries if entry.resolved_path}
|
|
567
|
-
try:
|
|
568
|
-
files_on_disk = {
|
|
569
|
-
path.resolve(): path
|
|
570
|
-
for path in directory.glob("*.jsonl")
|
|
571
|
-
if path.is_file() and path.name != MANIFEST_FILENAME
|
|
572
|
-
}
|
|
573
|
-
except OSError:
|
|
574
|
-
return False, total_bytes
|
|
575
|
-
|
|
576
|
-
changed = False
|
|
577
|
-
for resolved, actual in files_on_disk.items():
|
|
578
|
-
if resolved in resolved_paths:
|
|
579
|
-
continue
|
|
580
|
-
orphan_entry = _build_orphan_entry(
|
|
581
|
-
actual,
|
|
582
|
-
existing_ids=existing_ids,
|
|
583
|
-
current_run_id=current_run_id,
|
|
584
|
-
)
|
|
585
|
-
normalized_entries.append(orphan_entry)
|
|
586
|
-
persisted_entries.append(orphan_entry.persisted)
|
|
587
|
-
counters["orphans"] = counters.get("orphans", 0) + 1
|
|
588
|
-
if orphan_entry.history.status == "cached":
|
|
589
|
-
counters["cached"] = counters.get("cached", 0) + 1
|
|
590
|
-
if orphan_entry.history.size_bytes:
|
|
591
|
-
total_bytes += int(orphan_entry.history.size_bytes or 0)
|
|
592
|
-
changed = True
|
|
593
|
-
|
|
594
|
-
return changed, total_bytes
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def _normalise_manifest(
|
|
598
|
-
*,
|
|
599
|
-
cache_dir: Path | None,
|
|
600
|
-
current_run_id: str | None,
|
|
601
|
-
include_orphans: bool,
|
|
602
|
-
) -> tuple[list[NormalizedEntry], list[dict[str, Any]], list[str], dict[str, int], Path, Path, bool, int]:
|
|
603
|
-
"""Return normalised entries plus bookkeeping metadata for a given cache directory."""
|
|
604
|
-
directory = ensure_cache_dir(cache_dir)
|
|
605
|
-
manifest = manifest_path(directory)
|
|
606
|
-
raw_entries = load_manifest_entries(directory)
|
|
607
|
-
(
|
|
608
|
-
normalized_entries,
|
|
609
|
-
persisted_entries,
|
|
610
|
-
warnings,
|
|
611
|
-
counters,
|
|
612
|
-
changed,
|
|
613
|
-
total_bytes,
|
|
614
|
-
existing_ids,
|
|
615
|
-
) = _process_manifest_entries(
|
|
616
|
-
raw_entries,
|
|
617
|
-
directory=directory,
|
|
618
|
-
current_run_id=current_run_id,
|
|
619
|
-
)
|
|
620
|
-
|
|
621
|
-
counters.setdefault("orphans", 0)
|
|
622
|
-
|
|
623
|
-
if include_orphans:
|
|
624
|
-
orphan_changed, total_bytes = _append_orphan_entries(
|
|
625
|
-
directory,
|
|
626
|
-
current_run_id=current_run_id,
|
|
627
|
-
existing_ids=existing_ids,
|
|
628
|
-
normalized_entries=normalized_entries,
|
|
629
|
-
persisted_entries=persisted_entries,
|
|
630
|
-
counters=counters,
|
|
631
|
-
total_bytes=total_bytes,
|
|
632
|
-
)
|
|
633
|
-
changed = changed or orphan_changed
|
|
634
|
-
|
|
635
|
-
return (
|
|
636
|
-
normalized_entries,
|
|
637
|
-
persisted_entries,
|
|
638
|
-
warnings,
|
|
639
|
-
counters,
|
|
640
|
-
directory,
|
|
641
|
-
manifest,
|
|
642
|
-
changed,
|
|
643
|
-
total_bytes,
|
|
644
|
-
)
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
def _resolve_history_default_limit() -> int:
|
|
648
|
-
"""Return the configured default limit from `aip config`, falling back to the built-in default."""
|
|
649
|
-
try:
|
|
650
|
-
config = load_config()
|
|
651
|
-
except Exception:
|
|
652
|
-
config = {}
|
|
653
|
-
config_limit = _to_int(config.get("history_default_limit"))
|
|
654
|
-
if config_limit is not None:
|
|
655
|
-
return max(0, config_limit)
|
|
656
|
-
|
|
657
|
-
return DEFAULT_HISTORY_LIMIT
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
def load_history_snapshot(
|
|
661
|
-
*,
|
|
662
|
-
limit: int | None = None,
|
|
663
|
-
ctx: Any | None = None,
|
|
664
|
-
cache_dir: Path | None = None,
|
|
665
|
-
) -> HistorySnapshot:
|
|
666
|
-
"""Load cached transcript history applying migrations as needed."""
|
|
667
|
-
ctx_obj = getattr(ctx, "obj", None)
|
|
668
|
-
current_run_id = None
|
|
669
|
-
if isinstance(ctx_obj, dict):
|
|
670
|
-
manifest = ctx_obj.get("_last_transcript_manifest")
|
|
671
|
-
if isinstance(manifest, dict):
|
|
672
|
-
run_id = manifest.get("run_id")
|
|
673
|
-
if run_id:
|
|
674
|
-
current_run_id = str(run_id)
|
|
675
|
-
|
|
676
|
-
(
|
|
677
|
-
normalized_entries,
|
|
678
|
-
persisted_entries,
|
|
679
|
-
warnings,
|
|
680
|
-
counters,
|
|
681
|
-
directory,
|
|
682
|
-
manifest_file,
|
|
683
|
-
changed,
|
|
684
|
-
total_bytes,
|
|
685
|
-
) = _normalise_manifest(cache_dir=cache_dir, current_run_id=current_run_id, include_orphans=True)
|
|
686
|
-
|
|
687
|
-
if changed:
|
|
688
|
-
write_manifest(persisted_entries, directory)
|
|
689
|
-
|
|
690
|
-
if limit is None or limit == 0:
|
|
691
|
-
requested_limit = _resolve_history_default_limit()
|
|
692
|
-
else:
|
|
693
|
-
requested_limit = max(0, int(limit))
|
|
694
|
-
|
|
695
|
-
limit_applied = requested_limit
|
|
696
|
-
limit_clamped = False
|
|
697
|
-
if limit_applied > MAX_HISTORY_LIMIT:
|
|
698
|
-
limit_applied = MAX_HISTORY_LIMIT
|
|
699
|
-
limit_clamped = True
|
|
700
|
-
|
|
701
|
-
entries_sorted = sorted(
|
|
702
|
-
(entry.history for entry in normalized_entries),
|
|
703
|
-
key=lambda h: coerce_sortable_datetime(h.started_at),
|
|
704
|
-
reverse=True,
|
|
705
|
-
)
|
|
706
|
-
|
|
707
|
-
display_entries = entries_sorted[:limit_applied] if limit_applied else []
|
|
708
|
-
entries_index = {entry.history.run_id: entry.history for entry in normalized_entries if entry.history.run_id}
|
|
709
|
-
|
|
710
|
-
migration_summary = _format_migration_summary(counters)
|
|
711
|
-
|
|
712
|
-
return HistorySnapshot(
|
|
713
|
-
manifest_path=manifest_file,
|
|
714
|
-
entries=display_entries,
|
|
715
|
-
total_entries=len(entries_sorted),
|
|
716
|
-
cached_entries=counters.get("cached", 0),
|
|
717
|
-
total_size_bytes=total_bytes,
|
|
718
|
-
index=entries_index,
|
|
719
|
-
warnings=warnings,
|
|
720
|
-
migration_summary=migration_summary,
|
|
721
|
-
limit_requested=requested_limit,
|
|
722
|
-
limit_applied=limit_applied,
|
|
723
|
-
limit_clamped=limit_clamped,
|
|
724
|
-
)
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
def _normalise_run_ids(run_ids: Sequence[str] | None, entries_by_run: dict[str, NormalizedEntry]) -> list[str]:
|
|
728
|
-
"""Return a deduplicated list of run ids requested for deletion."""
|
|
729
|
-
if run_ids is None:
|
|
730
|
-
return list(entries_by_run.keys())
|
|
731
|
-
seen: set[str] = set()
|
|
732
|
-
deduped: list[str] = []
|
|
733
|
-
for run_id in run_ids:
|
|
734
|
-
if run_id in seen:
|
|
735
|
-
continue
|
|
736
|
-
seen.add(run_id)
|
|
737
|
-
deduped.append(run_id)
|
|
738
|
-
return deduped
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
def _purge_entry(entry: NormalizedEntry) -> tuple[HistoryEntry, int, list[str]]:
|
|
742
|
-
"""Remove a cached transcript from disk and return bookkeeping information."""
|
|
743
|
-
reclaimed_bytes = 0
|
|
744
|
-
warnings: list[str] = []
|
|
745
|
-
resolved = entry.resolved_path
|
|
746
|
-
if resolved and resolved.exists():
|
|
747
|
-
try:
|
|
748
|
-
reclaimed_bytes = resolved.stat().st_size
|
|
749
|
-
resolved.unlink()
|
|
750
|
-
except FileNotFoundError:
|
|
751
|
-
pass
|
|
752
|
-
except OSError as exc:
|
|
753
|
-
warnings.append(f"Failed to remove {resolved}: {exc}")
|
|
754
|
-
else:
|
|
755
|
-
warnings.append(
|
|
756
|
-
f"Transcript file already missing for run {entry.history.run_id}. Manifest entry will be removed."
|
|
757
|
-
)
|
|
758
|
-
return entry.history, reclaimed_bytes, warnings
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
def clear_cached_runs(
|
|
762
|
-
run_ids: Sequence[str] | None,
|
|
763
|
-
*,
|
|
764
|
-
cache_dir: Path | None = None,
|
|
765
|
-
) -> ClearResult:
|
|
766
|
-
"""Remove cached transcripts and update the manifest."""
|
|
767
|
-
(
|
|
768
|
-
normalized_entries,
|
|
769
|
-
persisted_entries,
|
|
770
|
-
warnings,
|
|
771
|
-
_counters,
|
|
772
|
-
directory,
|
|
773
|
-
manifest_file,
|
|
774
|
-
changed,
|
|
775
|
-
_total_bytes,
|
|
776
|
-
) = _normalise_manifest(cache_dir=cache_dir, current_run_id=None, include_orphans=True)
|
|
777
|
-
|
|
778
|
-
# If normalisation changed entries, update manifest before processing deletions
|
|
779
|
-
if changed:
|
|
780
|
-
write_manifest(persisted_entries, directory)
|
|
781
|
-
|
|
782
|
-
entries_by_run = {entry.history.run_id: entry for entry in normalized_entries if entry.history.run_id}
|
|
783
|
-
|
|
784
|
-
target_run_ids = _normalise_run_ids(run_ids, entries_by_run)
|
|
785
|
-
missing = [run_id for run_id in target_run_ids if run_id not in entries_by_run]
|
|
786
|
-
removable = {run_id for run_id in target_run_ids if run_id in entries_by_run}
|
|
787
|
-
|
|
788
|
-
removed_entries: list[HistoryEntry] = []
|
|
789
|
-
reclaimed_bytes = 0
|
|
790
|
-
additional_warnings: list[str] = []
|
|
791
|
-
remaining_entries: list[dict[str, Any]] = []
|
|
792
|
-
|
|
793
|
-
for entry in normalized_entries:
|
|
794
|
-
run_id = entry.history.run_id
|
|
795
|
-
if run_id in removable:
|
|
796
|
-
history_entry, reclaimed, warnings_extra = _purge_entry(entry)
|
|
797
|
-
removed_entries.append(history_entry)
|
|
798
|
-
reclaimed_bytes += reclaimed
|
|
799
|
-
additional_warnings.extend(warnings_extra)
|
|
800
|
-
else:
|
|
801
|
-
remaining_entries.append(entry.persisted)
|
|
802
|
-
|
|
803
|
-
write_manifest(remaining_entries, directory)
|
|
804
|
-
|
|
805
|
-
combined_warnings = warnings + additional_warnings
|
|
806
|
-
cache_empty = len(remaining_entries) == 0
|
|
807
|
-
|
|
808
|
-
return ClearResult(
|
|
809
|
-
manifest_path=manifest_file,
|
|
810
|
-
removed_entries=removed_entries,
|
|
811
|
-
not_found=missing,
|
|
812
|
-
warnings=combined_warnings,
|
|
813
|
-
reclaimed_bytes=reclaimed_bytes,
|
|
814
|
-
cache_empty=cache_empty,
|
|
815
|
-
)
|