multi-forge 0.2.0__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.
- forge/__init__.py +3 -0
- forge/_extensions/agents/.gitkeep +0 -0
- forge/_extensions/commands/.gitkeep +0 -0
- forge/_extensions/skills/analyze/SKILL.md +87 -0
- forge/_extensions/skills/challenge/SKILL.md +91 -0
- forge/_extensions/skills/consensus/SKILL.md +120 -0
- forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
- forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
- forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
- forge/_extensions/skills/debate/SKILL.md +116 -0
- forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
- forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
- forge/_extensions/skills/panel/SKILL.md +141 -0
- forge/_extensions/skills/panel/resources/synthesis.md +103 -0
- forge/_extensions/skills/qa/SKILL.md +704 -0
- forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
- forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
- forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
- forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
- forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
- forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
- forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
- forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
- forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
- forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
- forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
- forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
- forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
- forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
- forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
- forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
- forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
- forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
- forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
- forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
- forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
- forge/_extensions/skills/qa/resources/checklist.md +103 -0
- forge/_extensions/skills/qa/resources/report-template.md +62 -0
- forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
- forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
- forge/_extensions/skills/review/SKILL.md +125 -0
- forge/_extensions/skills/review/references/claude-4.6.md +474 -0
- forge/_extensions/skills/review/references/claude-4.7.md +710 -0
- forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
- forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
- forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
- forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
- forge/_extensions/skills/review/resources/code-gemini.md +184 -0
- forge/_extensions/skills/review/resources/code-openai.md +203 -0
- forge/_extensions/skills/review/resources/code.md +160 -0
- forge/_extensions/skills/review-docs/SKILL.md +121 -0
- forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
- forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
- forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
- forge/_extensions/skills/review-docs/resources/docs.md +170 -0
- forge/_extensions/skills/smoke-test/SKILL.md +27 -0
- forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
- forge/_extensions/skills/understand/SKILL.md +148 -0
- forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
- forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
- forge/_extensions/skills/understand/resources/code-openai.md +181 -0
- forge/_extensions/skills/understand/resources/code.md +163 -0
- forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
- forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
- forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
- forge/_extensions/skills/understand/resources/docs.md +177 -0
- forge/_extensions/skills/walkthrough/SKILL.md +599 -0
- forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
- forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
- forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
- forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
- forge/backend/__init__.py +174 -0
- forge/backend/adapters/__init__.py +38 -0
- forge/backend/adapters/litellm.py +158 -0
- forge/backend/creation.py +89 -0
- forge/backend/registry.py +178 -0
- forge/cli/__init__.py +16 -0
- forge/cli/auth.py +483 -0
- forge/cli/backend.py +298 -0
- forge/cli/claude.py +411 -0
- forge/cli/config_cmd.py +303 -0
- forge/cli/extensions.py +1001 -0
- forge/cli/gc.py +165 -0
- forge/cli/guard.py +1018 -0
- forge/cli/guards.py +106 -0
- forge/cli/handoff.py +110 -0
- forge/cli/hooks/__init__.py +36 -0
- forge/cli/hooks/_group.py +20 -0
- forge/cli/hooks/_helpers.py +149 -0
- forge/cli/hooks/commands.py +1677 -0
- forge/cli/hooks/direct_commands.py +1304 -0
- forge/cli/hooks/install.py +232 -0
- forge/cli/hooks/policy.py +151 -0
- forge/cli/hooks/read_hygiene.py +74 -0
- forge/cli/hooks/verification.py +370 -0
- forge/cli/logs.py +406 -0
- forge/cli/main.py +292 -0
- forge/cli/proxy.py +1821 -0
- forge/cli/proxy_costs.py +313 -0
- forge/cli/search.py +416 -0
- forge/cli/session.py +892 -0
- forge/cli/session_addendum.py +81 -0
- forge/cli/session_fork.py +750 -0
- forge/cli/session_handoff.py +141 -0
- forge/cli/session_lifecycle.py +2053 -0
- forge/cli/session_manage.py +1336 -0
- forge/cli/session_memory.py +201 -0
- forge/cli/status_line.py +1398 -0
- forge/cli/workflow.py +1964 -0
- forge/config/__init__.py +110 -0
- forge/config/dataclass_utils.py +88 -0
- forge/config/defaults/__init__.py +0 -0
- forge/config/defaults/backends/__init__.py +0 -0
- forge/config/defaults/backends/litellm.yaml +196 -0
- forge/config/defaults/templates/__init__.py +0 -0
- forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
- forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
- forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
- forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
- forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
- forge/config/defaults/templates/litellm-gemini.yaml +21 -0
- forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
- forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
- forge/config/defaults/templates/litellm-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
- forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
- forge/config/defaults/templates/openrouter-glm.yaml +23 -0
- forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
- forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
- forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
- forge/config/defaults/templates/openrouter-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
- forge/config/loader.py +675 -0
- forge/config/schema.py +448 -0
- forge/core/__init__.py +5 -0
- forge/core/auth/__init__.py +67 -0
- forge/core/auth/capabilities.py +219 -0
- forge/core/auth/credentials_file.py +244 -0
- forge/core/auth/protocols.py +18 -0
- forge/core/auth/secrets.py +243 -0
- forge/core/auth/template_secrets.py +112 -0
- forge/core/data/__init__.py +5 -0
- forge/core/data/model_catalog.yaml +1522 -0
- forge/core/data/pricing.yaml +140 -0
- forge/core/data/system_prompt_addendums/__init__.py +0 -0
- forge/core/data/system_prompt_addendums/gemini.md +330 -0
- forge/core/data/system_prompt_addendums/openai.md +328 -0
- forge/core/llm/__init__.py +231 -0
- forge/core/llm/clients/__init__.py +14 -0
- forge/core/llm/clients/base.py +115 -0
- forge/core/llm/clients/litellm.py +619 -0
- forge/core/llm/clients/openai_compat.py +244 -0
- forge/core/llm/clients/openrouter.py +234 -0
- forge/core/llm/credentials.py +439 -0
- forge/core/llm/detection.py +86 -0
- forge/core/llm/errors.py +44 -0
- forge/core/llm/protocols.py +80 -0
- forge/core/llm/types.py +176 -0
- forge/core/logging.py +146 -0
- forge/core/models/__init__.py +91 -0
- forge/core/models/catalog.py +467 -0
- forge/core/models/pricing.py +165 -0
- forge/core/models/types.py +167 -0
- forge/core/naming.py +212 -0
- forge/core/ops/__init__.py +73 -0
- forge/core/ops/context.py +141 -0
- forge/core/ops/gc.py +802 -0
- forge/core/ops/proxy.py +146 -0
- forge/core/ops/resolution.py +135 -0
- forge/core/ops/session.py +344 -0
- forge/core/ops/session_context.py +548 -0
- forge/core/paths.py +38 -0
- forge/core/process.py +54 -0
- forge/core/reactive/__init__.py +38 -0
- forge/core/reactive/cost_tracking.py +300 -0
- forge/core/reactive/env.py +180 -0
- forge/core/reactive/proxy.py +78 -0
- forge/core/reactive/routing.py +622 -0
- forge/core/reactive/session_runner.py +185 -0
- forge/core/reactive/structured_output.py +62 -0
- forge/core/reactive/tagger.py +94 -0
- forge/core/reactive/throttle.py +132 -0
- forge/core/state/__init__.py +59 -0
- forge/core/state/exceptions.py +59 -0
- forge/core/state/io.py +140 -0
- forge/core/state/lock.py +99 -0
- forge/core/state/timestamps.py +60 -0
- forge/core/transcript.py +78 -0
- forge/core/typing_helpers.py +24 -0
- forge/core/workqueue/__init__.py +67 -0
- forge/core/workqueue/queue.py +552 -0
- forge/core/workqueue/types.py +63 -0
- forge/guard/__init__.py +26 -0
- forge/guard/deterministic/__init__.py +26 -0
- forge/guard/deterministic/base.py +158 -0
- forge/guard/deterministic/coding_standards.py +256 -0
- forge/guard/deterministic/registry.py +148 -0
- forge/guard/deterministic/tdd.py +171 -0
- forge/guard/engine.py +216 -0
- forge/guard/protocols.py +91 -0
- forge/guard/queries.py +96 -0
- forge/guard/semantic/__init__.py +34 -0
- forge/guard/semantic/promotion.py +18 -0
- forge/guard/semantic/supervisor.py +813 -0
- forge/guard/semantic/verdict.py +183 -0
- forge/guard/store.py +124 -0
- forge/guard/team/__init__.py +6 -0
- forge/guard/team/config.py +24 -0
- forge/guard/team/handlers.py +209 -0
- forge/guard/team/prompts.py +41 -0
- forge/guard/types.py +125 -0
- forge/guard/workflow/__init__.py +17 -0
- forge/guard/workflow/branches.py +67 -0
- forge/guard/workflow/config.py +63 -0
- forge/guard/workflow/divergence.py +113 -0
- forge/guard/workflow/policy.py +87 -0
- forge/guard/workflow/stages.py +205 -0
- forge/install/__init__.py +55 -0
- forge/install/cli.py +281 -0
- forge/install/exceptions.py +163 -0
- forge/install/hooks.py +109 -0
- forge/install/installer.py +1037 -0
- forge/install/models.py +321 -0
- forge/install/preset.py +272 -0
- forge/install/settings_merge.py +831 -0
- forge/install/tracking.py +238 -0
- forge/install/version.py +141 -0
- forge/proxy/__init__.py +0 -0
- forge/proxy/base_client.py +181 -0
- forge/proxy/client_adapter.py +476 -0
- forge/proxy/client_factory.py +531 -0
- forge/proxy/converters.py +1206 -0
- forge/proxy/cost_logger.py +132 -0
- forge/proxy/cost_tracker.py +242 -0
- forge/proxy/data_models.py +338 -0
- forge/proxy/error_hints.py +92 -0
- forge/proxy/metrics.py +222 -0
- forge/proxy/model_spec.py +158 -0
- forge/proxy/proxies.py +333 -0
- forge/proxy/proxy_identity.py +134 -0
- forge/proxy/proxy_orchestrator.py +1018 -0
- forge/proxy/proxy_startup.py +54 -0
- forge/proxy/server.py +1561 -0
- forge/proxy/utils.py +537 -0
- forge/review/__init__.py +6 -0
- forge/review/adversarial.py +111 -0
- forge/review/consensus.py +236 -0
- forge/review/engine.py +356 -0
- forge/review/models.py +437 -0
- forge/review/resources/__init__.py +5 -0
- forge/review/resources/codereview-performance.md +85 -0
- forge/review/resources/codereview-quick.md +75 -0
- forge/review/resources/codereview-security.md +92 -0
- forge/review/resources/codereview.md +85 -0
- forge/review/resources/docreview-quick.md +75 -0
- forge/review/resources/docreview.md +86 -0
- forge/review/resources/thinkdeep.md +89 -0
- forge/review/routing.py +368 -0
- forge/review/synthesis.py +73 -0
- forge/runtime_config.py +438 -0
- forge/search/__init__.py +55 -0
- forge/search/bm25_store.py +264 -0
- forge/search/content_store.py +197 -0
- forge/search/engine.py +352 -0
- forge/search/exceptions.py +51 -0
- forge/search/extractor.py +234 -0
- forge/search/index_state.py +295 -0
- forge/search/store.py +215 -0
- forge/search/tokenizer.py +24 -0
- forge/session/__init__.py +130 -0
- forge/session/active.py +339 -0
- forge/session/artifacts.py +202 -0
- forge/session/claude/__init__.py +50 -0
- forge/session/claude/cleanup.py +105 -0
- forge/session/claude/invoke.py +236 -0
- forge/session/claude/paths.py +200 -0
- forge/session/cleanup.py +216 -0
- forge/session/config.py +34 -0
- forge/session/direct_model.py +107 -0
- forge/session/effective.py +169 -0
- forge/session/exceptions.py +255 -0
- forge/session/handoff.py +881 -0
- forge/session/handoff_agent.py +544 -0
- forge/session/hooks/__init__.py +35 -0
- forge/session/hooks/models.py +73 -0
- forge/session/hooks/session_start.py +507 -0
- forge/session/identity.py +84 -0
- forge/session/index.py +553 -0
- forge/session/manager.py +1506 -0
- forge/session/models.py +572 -0
- forge/session/overrides.py +344 -0
- forge/session/plan_resolution.py +286 -0
- forge/session/prev_sessions.py +128 -0
- forge/session/store.py +431 -0
- forge/session/validation.py +47 -0
- forge/session/worktree/__init__.py +65 -0
- forge/session/worktree/cleanup.py +262 -0
- forge/session/worktree/config_copy.py +203 -0
- forge/session/worktree/create.py +332 -0
- forge/sidecar/__init__.py +29 -0
- forge/sidecar/container.py +161 -0
- forge/sidecar/docker.py +86 -0
- forge/sidecar/secrets.py +19 -0
- multi_forge-0.2.0.dist-info/METADATA +242 -0
- multi_forge-0.2.0.dist-info/RECORD +311 -0
- multi_forge-0.2.0.dist-info/WHEEL +4 -0
- multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
- multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
- multi_forge-0.2.0.dist-info/licenses/NOTICE +14 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Search index state for incremental transcript indexing.
|
|
2
|
+
|
|
3
|
+
Tracks which transcript files have been indexed via mtime/size fingerprints,
|
|
4
|
+
enabling the search system to skip already-indexed files and detect changes.
|
|
5
|
+
|
|
6
|
+
Two-layer architecture:
|
|
7
|
+
- IndexState (dataclass): pure in-memory operations (needs_reindex, mark_indexed, prune)
|
|
8
|
+
- IndexStateStore: persistence + locking (read, write, update with file_lock_for_target)
|
|
9
|
+
|
|
10
|
+
State file location: <project_root>/.forge/search-index/state.json
|
|
11
|
+
|
|
12
|
+
Follows the BackendRegistry/BackendRegistryStore pattern from forge.backend.registry.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
from dataclasses import asdict, dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Callable
|
|
22
|
+
|
|
23
|
+
from forge.core.state import (
|
|
24
|
+
SchemaVersionError,
|
|
25
|
+
atomic_write_json,
|
|
26
|
+
file_lock_for_target,
|
|
27
|
+
now_iso,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .exceptions import IndexStateCorruptedError
|
|
31
|
+
|
|
32
|
+
# Directory and file names
|
|
33
|
+
SEARCH_INDEX_DIR = "search-index"
|
|
34
|
+
STATE_FILENAME = "state.json"
|
|
35
|
+
|
|
36
|
+
# Schema version — reject anything else (no migration, per coding-standards.md)
|
|
37
|
+
INDEX_STATE_VERSION = 1
|
|
38
|
+
|
|
39
|
+
# Lock timeouts
|
|
40
|
+
CLI_LOCK_TIMEOUT_S = 5.0
|
|
41
|
+
HANDLER_LOCK_TIMEOUT_S = 1.0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_project_index_state_path(forge_root: Path) -> Path:
|
|
45
|
+
"""Return the index state path for a Forge project.
|
|
46
|
+
|
|
47
|
+
Path: <forge_root>/.forge/search-index/state.json
|
|
48
|
+
"""
|
|
49
|
+
return forge_root / ".forge" / SEARCH_INDEX_DIR / STATE_FILENAME
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _require_absolute(path: Path) -> None:
|
|
53
|
+
"""Raise ValueError if path is not absolute."""
|
|
54
|
+
if not path.is_absolute():
|
|
55
|
+
raise ValueError(f"path must be absolute, got: {path}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --- Data layer ---
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class IndexedFileEntry:
|
|
63
|
+
"""Tracking metadata for a single indexed transcript file."""
|
|
64
|
+
|
|
65
|
+
mtime: float
|
|
66
|
+
size: int
|
|
67
|
+
indexed_at: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class IndexState:
|
|
72
|
+
"""In-memory representation of the search index state.
|
|
73
|
+
|
|
74
|
+
Pure operations — no disk I/O. Persistence is handled by IndexStateStore.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
schema_version: int = INDEX_STATE_VERSION
|
|
78
|
+
updated_at: str = ""
|
|
79
|
+
indexed_files: dict[str, IndexedFileEntry] = field(default_factory=dict)
|
|
80
|
+
|
|
81
|
+
def needs_reindex(self, path: Path) -> bool:
|
|
82
|
+
"""Check if a file needs (re)indexing based on mtime/size.
|
|
83
|
+
|
|
84
|
+
Returns True if the file is new or has changed since last indexing.
|
|
85
|
+
Returns False if the file is unchanged OR if the file does not exist on disk.
|
|
86
|
+
|
|
87
|
+
Missing-file semantics:
|
|
88
|
+
- No entry + file missing → False (nothing to do)
|
|
89
|
+
- Entry exists + file deleted → False (prune_missing() handles cleanup)
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If path is not absolute.
|
|
93
|
+
"""
|
|
94
|
+
_require_absolute(path)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
stat = os.stat(path)
|
|
98
|
+
except OSError:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
key = str(path)
|
|
102
|
+
entry = self.indexed_files.get(key)
|
|
103
|
+
if entry is None:
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
return entry.mtime != stat.st_mtime or entry.size != stat.st_size
|
|
107
|
+
|
|
108
|
+
def mark_indexed(self, path: Path) -> None:
|
|
109
|
+
"""Record that a file has been indexed with its current mtime/size.
|
|
110
|
+
|
|
111
|
+
Creates or updates the entry for the given path using the file's
|
|
112
|
+
current stat() values and the current timestamp.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: If path is not absolute.
|
|
116
|
+
FileNotFoundError: If path does not exist on disk.
|
|
117
|
+
"""
|
|
118
|
+
_require_absolute(path)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
stat = os.stat(path)
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
raise
|
|
124
|
+
except OSError as e:
|
|
125
|
+
raise FileNotFoundError(str(path)) from e
|
|
126
|
+
|
|
127
|
+
self.indexed_files[str(path)] = IndexedFileEntry(
|
|
128
|
+
mtime=stat.st_mtime,
|
|
129
|
+
size=stat.st_size,
|
|
130
|
+
indexed_at=now_iso(),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def prune_missing(self) -> list[str]:
|
|
134
|
+
"""Remove entries for files that no longer exist on disk.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of path strings that were removed.
|
|
138
|
+
"""
|
|
139
|
+
to_remove = [key for key in self.indexed_files if not Path(key).is_file()]
|
|
140
|
+
for key in to_remove:
|
|
141
|
+
del self.indexed_files[key]
|
|
142
|
+
return to_remove
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# --- Persistence layer ---
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class IndexStateStore:
|
|
149
|
+
"""Manage per-project search index state.
|
|
150
|
+
|
|
151
|
+
Store location: <project_root>/.forge/search-index/state.json
|
|
152
|
+
Tracks which transcript files have been indexed for incremental updates.
|
|
153
|
+
Uses atomic writes and advisory file locking for concurrent safety.
|
|
154
|
+
|
|
155
|
+
Error handling:
|
|
156
|
+
- Missing file: returns empty state (self-healing)
|
|
157
|
+
- Corrupted file: raises IndexStateCorruptedError
|
|
158
|
+
- Wrong schema version: raises SchemaVersionError
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
forge_root: Path | None = None,
|
|
164
|
+
*,
|
|
165
|
+
state_path: Path | None = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
if state_path:
|
|
168
|
+
self._state_path = state_path # Explicit override (tests)
|
|
169
|
+
elif forge_root:
|
|
170
|
+
self._state_path = get_project_index_state_path(forge_root)
|
|
171
|
+
else:
|
|
172
|
+
raise ValueError("Either forge_root or state_path required")
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def state_path(self) -> Path:
|
|
176
|
+
return self._state_path
|
|
177
|
+
|
|
178
|
+
def exists(self) -> bool:
|
|
179
|
+
return self._state_path.is_file()
|
|
180
|
+
|
|
181
|
+
def read(self) -> IndexState:
|
|
182
|
+
"""Read the index state from disk.
|
|
183
|
+
|
|
184
|
+
Returns empty IndexState if the file does not exist (self-healing).
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
IndexStateCorruptedError: If the file contains invalid JSON or structure.
|
|
188
|
+
SchemaVersionError: If the schema version doesn't match INDEX_STATE_VERSION.
|
|
189
|
+
"""
|
|
190
|
+
if not self.exists():
|
|
191
|
+
return IndexState()
|
|
192
|
+
|
|
193
|
+
path_str = str(self._state_path)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
with open(self._state_path, encoding="utf-8") as f:
|
|
197
|
+
data = json.load(f)
|
|
198
|
+
except json.JSONDecodeError as e:
|
|
199
|
+
raise IndexStateCorruptedError(path_str, f"invalid JSON: {e}") from e
|
|
200
|
+
except OSError as e:
|
|
201
|
+
raise IndexStateCorruptedError(path_str, f"read error: {e}") from e
|
|
202
|
+
|
|
203
|
+
if not isinstance(data, dict):
|
|
204
|
+
raise IndexStateCorruptedError(path_str, f"expected JSON object, got {type(data).__name__}")
|
|
205
|
+
|
|
206
|
+
version = data.get("schema_version")
|
|
207
|
+
if version is None:
|
|
208
|
+
raise IndexStateCorruptedError(path_str, "missing schema_version")
|
|
209
|
+
if version != INDEX_STATE_VERSION:
|
|
210
|
+
raise SchemaVersionError(path_str, INDEX_STATE_VERSION, version)
|
|
211
|
+
|
|
212
|
+
# Deserialize indexed_files: dict[str, dict] → dict[str, IndexedFileEntry]
|
|
213
|
+
indexed_files: dict[str, IndexedFileEntry] = {}
|
|
214
|
+
raw_files = data.get("indexed_files", {})
|
|
215
|
+
if isinstance(raw_files, dict):
|
|
216
|
+
for key, val in raw_files.items():
|
|
217
|
+
if isinstance(val, dict):
|
|
218
|
+
try:
|
|
219
|
+
indexed_files[key] = IndexedFileEntry(
|
|
220
|
+
mtime=float(val["mtime"]),
|
|
221
|
+
size=int(val["size"]),
|
|
222
|
+
indexed_at=str(val.get("indexed_at", "")),
|
|
223
|
+
)
|
|
224
|
+
except (KeyError, TypeError, ValueError):
|
|
225
|
+
# Skip malformed entries rather than failing the whole read
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
return IndexState(
|
|
229
|
+
schema_version=version,
|
|
230
|
+
updated_at=data.get("updated_at", ""),
|
|
231
|
+
indexed_files=indexed_files,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def write(self, state: IndexState) -> None:
|
|
235
|
+
"""Write the index state atomically.
|
|
236
|
+
|
|
237
|
+
Sets state.updated_at to the current timestamp before writing.
|
|
238
|
+
Creates parent directories if needed.
|
|
239
|
+
"""
|
|
240
|
+
state.updated_at = now_iso()
|
|
241
|
+
data = asdict(state)
|
|
242
|
+
atomic_write_json(self._state_path, data)
|
|
243
|
+
|
|
244
|
+
def update(self, *, timeout_s: float, mutate: Callable[[IndexState], None]) -> IndexState:
|
|
245
|
+
"""Locked read-modify-write cycle.
|
|
246
|
+
|
|
247
|
+
Acquires an advisory file lock, reads the state, calls mutate(state),
|
|
248
|
+
then writes the updated state. Exceptions from mutate propagate
|
|
249
|
+
(not swallowed).
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
timeout_s: Maximum time to wait for the lock.
|
|
253
|
+
mutate: Callable that modifies the IndexState in-place.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
The updated IndexState after mutation and write.
|
|
257
|
+
"""
|
|
258
|
+
with file_lock_for_target(target_path=self._state_path, timeout_s=timeout_s):
|
|
259
|
+
state = self.read()
|
|
260
|
+
mutate(state)
|
|
261
|
+
self.write(state)
|
|
262
|
+
return state
|
|
263
|
+
|
|
264
|
+
# -- Convenience wrappers --
|
|
265
|
+
|
|
266
|
+
def mark_indexed(self, path: Path, *, timeout_s: float = HANDLER_LOCK_TIMEOUT_S) -> None:
|
|
267
|
+
"""Mark a file as indexed (locked read-modify-write).
|
|
268
|
+
|
|
269
|
+
Convenience wrapper around update() that calls state.mark_indexed(path).
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: If path is not absolute.
|
|
273
|
+
FileNotFoundError: If path does not exist on disk.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def _mutate(state: IndexState) -> None:
|
|
277
|
+
state.mark_indexed(path)
|
|
278
|
+
|
|
279
|
+
self.update(timeout_s=timeout_s, mutate=_mutate)
|
|
280
|
+
|
|
281
|
+
def prune_missing(self, *, timeout_s: float = CLI_LOCK_TIMEOUT_S) -> list[str]:
|
|
282
|
+
"""Remove entries for deleted files (locked read-modify-write).
|
|
283
|
+
|
|
284
|
+
Convenience wrapper around update() that calls state.prune_missing().
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of path strings that were removed.
|
|
288
|
+
"""
|
|
289
|
+
removed: list[str] = []
|
|
290
|
+
|
|
291
|
+
def _mutate(state: IndexState) -> None:
|
|
292
|
+
removed.extend(state.prune_missing())
|
|
293
|
+
|
|
294
|
+
self.update(timeout_s=timeout_s, mutate=_mutate)
|
|
295
|
+
return removed
|
forge/search/store.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Document metadata store for search-indexed transcripts (v2).
|
|
2
|
+
|
|
3
|
+
Persists SearchDocumentMeta objects (metadata only — no content, no tokens)
|
|
4
|
+
at <project_root>/.forge/search-index/documents.json. Content and BM25 index
|
|
5
|
+
data are stored in separate files (content.json, bm25_index.json).
|
|
6
|
+
|
|
7
|
+
Each project has its own store — no cross-project mixing in a single file.
|
|
8
|
+
|
|
9
|
+
Follows the IndexStateStore pattern: versioned JSON, atomic writes, file locking,
|
|
10
|
+
self-healing on missing file.
|
|
11
|
+
|
|
12
|
+
Uses dacite for deserialization (consistent with BackendRegistryStore).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import dacite
|
|
23
|
+
|
|
24
|
+
from forge.core.state import (
|
|
25
|
+
SchemaVersionError,
|
|
26
|
+
atomic_write_json,
|
|
27
|
+
file_lock_for_target,
|
|
28
|
+
now_iso,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from .exceptions import SearchDocumentStoreCorruptedError
|
|
32
|
+
from .extractor import SearchDocumentMeta
|
|
33
|
+
from .index_state import SEARCH_INDEX_DIR
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# File and schema constants
|
|
38
|
+
DOCUMENTS_FILENAME = "documents.json"
|
|
39
|
+
DOCUMENT_STORE_VERSION = 1
|
|
40
|
+
|
|
41
|
+
# Lock timeouts
|
|
42
|
+
STORE_LOCK_TIMEOUT_S = 5.0
|
|
43
|
+
HANDLER_STORE_LOCK_TIMEOUT_S = 1.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_project_documents_store_path(forge_root: Path) -> Path:
|
|
47
|
+
"""Return the document store path for a Forge project.
|
|
48
|
+
|
|
49
|
+
Path: <forge_root>/.forge/search-index/documents.json
|
|
50
|
+
"""
|
|
51
|
+
return forge_root / ".forge" / SEARCH_INDEX_DIR / DOCUMENTS_FILENAME
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SearchDocumentStore:
|
|
55
|
+
"""Manage per-project search document metadata store.
|
|
56
|
+
|
|
57
|
+
Store location: <forge_root>/.forge/search-index/documents.json
|
|
58
|
+
Documents are keyed by transcript_path (absolute path string).
|
|
59
|
+
|
|
60
|
+
V2 schema: metadata only (no content, no tokens). Content and BM25
|
|
61
|
+
index data are stored in separate files.
|
|
62
|
+
|
|
63
|
+
Error handling:
|
|
64
|
+
- Missing file: returns empty list (self-healing)
|
|
65
|
+
- Corrupted file: raises SearchDocumentStoreCorruptedError
|
|
66
|
+
- Wrong schema version: raises SchemaVersionError (v1 triggers rebuild)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
forge_root: Path | None = None,
|
|
72
|
+
*,
|
|
73
|
+
store_path: Path | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
if store_path:
|
|
76
|
+
self._store_path = store_path # Explicit override (tests)
|
|
77
|
+
elif forge_root:
|
|
78
|
+
self._store_path = get_project_documents_store_path(forge_root)
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError("Either forge_root or store_path required")
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def store_path(self) -> Path:
|
|
84
|
+
return self._store_path
|
|
85
|
+
|
|
86
|
+
def exists(self) -> bool:
|
|
87
|
+
return self._store_path.is_file()
|
|
88
|
+
|
|
89
|
+
def read(self) -> list[SearchDocumentMeta]:
|
|
90
|
+
"""Read all document metadata from disk.
|
|
91
|
+
|
|
92
|
+
Returns empty list if the file does not exist (self-healing).
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
SearchDocumentStoreCorruptedError: If the file contains invalid JSON.
|
|
96
|
+
SchemaVersionError: If the schema version doesn't match (v1 → rebuild).
|
|
97
|
+
"""
|
|
98
|
+
if not self.exists():
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
path_str = str(self._store_path)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
with open(self._store_path, encoding="utf-8") as f:
|
|
105
|
+
data = json.load(f)
|
|
106
|
+
except json.JSONDecodeError as e:
|
|
107
|
+
raise SearchDocumentStoreCorruptedError(path_str, f"invalid JSON: {e}") from e
|
|
108
|
+
except OSError as e:
|
|
109
|
+
raise SearchDocumentStoreCorruptedError(path_str, f"read error: {e}") from e
|
|
110
|
+
|
|
111
|
+
if not isinstance(data, dict):
|
|
112
|
+
raise SearchDocumentStoreCorruptedError(path_str, f"expected JSON object, got {type(data).__name__}")
|
|
113
|
+
|
|
114
|
+
version = data.get("schema_version")
|
|
115
|
+
if version is None:
|
|
116
|
+
raise SearchDocumentStoreCorruptedError(path_str, "missing schema_version")
|
|
117
|
+
if version != DOCUMENT_STORE_VERSION:
|
|
118
|
+
raise SchemaVersionError(path_str, DOCUMENT_STORE_VERSION, version)
|
|
119
|
+
|
|
120
|
+
raw_docs = data.get("documents", [])
|
|
121
|
+
if not isinstance(raw_docs, list):
|
|
122
|
+
logger.warning(
|
|
123
|
+
"Document store %s has non-list 'documents' field (got %s), treating as empty",
|
|
124
|
+
path_str,
|
|
125
|
+
type(raw_docs).__name__,
|
|
126
|
+
)
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
documents: list[SearchDocumentMeta] = []
|
|
130
|
+
path_str = str(self._store_path)
|
|
131
|
+
for i, raw in enumerate(raw_docs):
|
|
132
|
+
if not isinstance(raw, dict):
|
|
133
|
+
raise SearchDocumentStoreCorruptedError(path_str, f"entry {i} is {type(raw).__name__}, expected dict")
|
|
134
|
+
try:
|
|
135
|
+
doc = dacite.from_dict(
|
|
136
|
+
data_class=SearchDocumentMeta,
|
|
137
|
+
data=raw,
|
|
138
|
+
config=dacite.Config(strict=True),
|
|
139
|
+
)
|
|
140
|
+
documents.append(doc)
|
|
141
|
+
except (dacite.DaciteError, KeyError, TypeError) as e:
|
|
142
|
+
raise SearchDocumentStoreCorruptedError(path_str, f"entry {i} deserialization error: {e}") from e
|
|
143
|
+
|
|
144
|
+
return documents
|
|
145
|
+
|
|
146
|
+
def write(self, documents: list[SearchDocumentMeta]) -> None:
|
|
147
|
+
"""Write documents atomically.
|
|
148
|
+
|
|
149
|
+
Creates parent directories if needed.
|
|
150
|
+
"""
|
|
151
|
+
data: dict[str, Any] = {
|
|
152
|
+
"schema_version": DOCUMENT_STORE_VERSION,
|
|
153
|
+
"updated_at": now_iso(),
|
|
154
|
+
"documents": [doc.to_dict() for doc in documents],
|
|
155
|
+
}
|
|
156
|
+
atomic_write_json(self._store_path, data)
|
|
157
|
+
|
|
158
|
+
def replace_all(
|
|
159
|
+
self,
|
|
160
|
+
documents: list[SearchDocumentMeta],
|
|
161
|
+
*,
|
|
162
|
+
timeout_s: float = STORE_LOCK_TIMEOUT_S,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Replace all documents under lock (for rebuild-index)."""
|
|
165
|
+
with file_lock_for_target(target_path=self._store_path, timeout_s=timeout_s):
|
|
166
|
+
self.write(documents)
|
|
167
|
+
|
|
168
|
+
def add(
|
|
169
|
+
self,
|
|
170
|
+
doc: SearchDocumentMeta,
|
|
171
|
+
*,
|
|
172
|
+
timeout_s: float = HANDLER_STORE_LOCK_TIMEOUT_S,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Add or replace a document (locked read-modify-write).
|
|
175
|
+
|
|
176
|
+
Documents are keyed by transcript_path. Idempotent: if a document
|
|
177
|
+
with the same transcript_path already exists, it is replaced.
|
|
178
|
+
"""
|
|
179
|
+
with file_lock_for_target(target_path=self._store_path, timeout_s=timeout_s):
|
|
180
|
+
docs = self.read()
|
|
181
|
+
docs = [d for d in docs if d.transcript_path != doc.transcript_path]
|
|
182
|
+
docs.append(doc)
|
|
183
|
+
self.write(docs)
|
|
184
|
+
|
|
185
|
+
def prune_missing(self, *, timeout_s: float = STORE_LOCK_TIMEOUT_S) -> list[str]:
|
|
186
|
+
"""Remove documents whose transcript_path no longer exists on disk.
|
|
187
|
+
|
|
188
|
+
Locked read-modify-write. Returns list of removed transcript_path strings.
|
|
189
|
+
Skips write if nothing was pruned.
|
|
190
|
+
"""
|
|
191
|
+
with file_lock_for_target(target_path=self._store_path, timeout_s=timeout_s):
|
|
192
|
+
docs = self.read()
|
|
193
|
+
kept: list[SearchDocumentMeta] = []
|
|
194
|
+
removed: list[str] = []
|
|
195
|
+
for d in docs:
|
|
196
|
+
if Path(d.transcript_path).is_file():
|
|
197
|
+
kept.append(d)
|
|
198
|
+
else:
|
|
199
|
+
removed.append(d.transcript_path)
|
|
200
|
+
if removed:
|
|
201
|
+
self.write(kept)
|
|
202
|
+
return removed
|
|
203
|
+
|
|
204
|
+
def remove(self, transcript_path: str, *, timeout_s: float = STORE_LOCK_TIMEOUT_S) -> bool:
|
|
205
|
+
"""Remove a document by transcript_path (locked read-modify-write).
|
|
206
|
+
|
|
207
|
+
Returns True if a document was found and removed, False otherwise.
|
|
208
|
+
"""
|
|
209
|
+
with file_lock_for_target(target_path=self._store_path, timeout_s=timeout_s):
|
|
210
|
+
docs = self.read()
|
|
211
|
+
filtered = [d for d in docs if d.transcript_path != transcript_path]
|
|
212
|
+
if len(filtered) == len(docs):
|
|
213
|
+
return False
|
|
214
|
+
self.write(filtered)
|
|
215
|
+
return True
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared tokenization for BM25 indexing and search.
|
|
2
|
+
|
|
3
|
+
Used by both extractor (token caching at extraction time) and engine
|
|
4
|
+
(query tokenization + snippet anchoring at search time).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
# Matches word-like tokens (letters, digits, underscores).
|
|
12
|
+
# Case-insensitive so _best_snippet() can iterate raw mixed-case content
|
|
13
|
+
# and match against lowercased query tokens.
|
|
14
|
+
TOKEN_RE = re.compile(r"[a-zA-Z0-9_]+")
|
|
15
|
+
|
|
16
|
+
MIN_TOKEN_LENGTH = 2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def tokenize(text: str) -> list[str]:
|
|
20
|
+
"""Simple word tokenizer for BM25.
|
|
21
|
+
|
|
22
|
+
Lowercase, extract alphanumeric+underscore tokens, filter short tokens.
|
|
23
|
+
"""
|
|
24
|
+
return [m for m in TOKEN_RE.findall(text.lower()) if len(m) >= MIN_TOKEN_LENGTH]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Forge Session - Named session management for Claude Code.
|
|
2
|
+
|
|
3
|
+
This module provides the essential public API for session management:
|
|
4
|
+
|
|
5
|
+
- SessionState: Core data structure for session state (intent + confirmed)
|
|
6
|
+
- SessionStore: Read/write .forge/sessions/<name>/forge.session.json
|
|
7
|
+
- IndexStore: Read/write ~/.forge/sessions/index.json
|
|
8
|
+
- SessionManager: High-level session orchestration
|
|
9
|
+
|
|
10
|
+
For specialized access, import from submodules directly:
|
|
11
|
+
|
|
12
|
+
- forge.session.models: All data models (SessionIntent, SessionConfirmed, Worktree, etc.)
|
|
13
|
+
- forge.session.effective: Effective config computation (apply_overrides, get_effective_value)
|
|
14
|
+
- forge.session.overrides: Override operations (validate_key, parse_value, expand_wildcard)
|
|
15
|
+
- forge.session.hooks: Hook integration (handle_session_start, HookResult, etc.)
|
|
16
|
+
- forge.session.exceptions: Full exception hierarchy
|
|
17
|
+
- forge.session.store: Store constants (MANIFEST_FILENAME, etc.)
|
|
18
|
+
- forge.session.index: Index constants (INDEX_DIR, etc.)
|
|
19
|
+
- forge.session.validation: Name validation constants (MIN/MAX_NAME_LENGTH)
|
|
20
|
+
- forge.session.config: Config constants (VALID_PROXY_TEMPLATES)
|
|
21
|
+
|
|
22
|
+
Quick Start:
|
|
23
|
+
from forge.session import (
|
|
24
|
+
create_session_state,
|
|
25
|
+
SessionStore,
|
|
26
|
+
IndexStore,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Create a new session state
|
|
30
|
+
state = create_session_state(
|
|
31
|
+
"my-session",
|
|
32
|
+
proxy_template="litellm-gemini",
|
|
33
|
+
proxy_base_url="http://localhost:8084",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Write state to worktree (per-session directory)
|
|
37
|
+
store = SessionStore("/path/to/worktree", "my-session")
|
|
38
|
+
store.write(state)
|
|
39
|
+
|
|
40
|
+
# Add to global index
|
|
41
|
+
index = IndexStore()
|
|
42
|
+
index.add_from_state(state, "/path/to/project")
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
# Config
|
|
48
|
+
from .active import (
|
|
49
|
+
ActiveSessionEntry,
|
|
50
|
+
ActiveSessionIndex,
|
|
51
|
+
ActiveSessionStore,
|
|
52
|
+
run_with_active_session,
|
|
53
|
+
track_active_session,
|
|
54
|
+
)
|
|
55
|
+
from .config import (
|
|
56
|
+
DEFAULT_PROXY_BASE_URL,
|
|
57
|
+
DEFAULT_PROXY_TEMPLATE,
|
|
58
|
+
LAUNCH_MODE_HOST,
|
|
59
|
+
LAUNCH_MODE_SIDECAR,
|
|
60
|
+
SIDECAR_RUNTIME_BASE_URL,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Effective config
|
|
64
|
+
from .effective import compute_effective_intent
|
|
65
|
+
|
|
66
|
+
# Exceptions (base + common operational)
|
|
67
|
+
from .exceptions import (
|
|
68
|
+
ForgeSessionError,
|
|
69
|
+
InvalidSessionNameError,
|
|
70
|
+
SessionExistsError,
|
|
71
|
+
SessionNotFoundError,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Index
|
|
75
|
+
from .index import IndexStore
|
|
76
|
+
|
|
77
|
+
# Manager
|
|
78
|
+
from .manager import SessionManager
|
|
79
|
+
|
|
80
|
+
# Models
|
|
81
|
+
from .models import (
|
|
82
|
+
SCHEMA_VERSION,
|
|
83
|
+
SessionIndexEntry,
|
|
84
|
+
SessionState,
|
|
85
|
+
create_session_state,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Overrides
|
|
89
|
+
from .overrides import clear_overrides, delete_override, set_override
|
|
90
|
+
|
|
91
|
+
# Store
|
|
92
|
+
from .store import SessionStore
|
|
93
|
+
|
|
94
|
+
# Validation
|
|
95
|
+
from .validation import validate_name
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
# Core types
|
|
99
|
+
"SessionState",
|
|
100
|
+
"SessionIndexEntry",
|
|
101
|
+
"create_session_state",
|
|
102
|
+
"SCHEMA_VERSION",
|
|
103
|
+
"ActiveSessionEntry",
|
|
104
|
+
"ActiveSessionIndex",
|
|
105
|
+
# Stores
|
|
106
|
+
"SessionStore",
|
|
107
|
+
"IndexStore",
|
|
108
|
+
"ActiveSessionStore",
|
|
109
|
+
# Manager
|
|
110
|
+
"SessionManager",
|
|
111
|
+
# Operations
|
|
112
|
+
"compute_effective_intent",
|
|
113
|
+
"set_override",
|
|
114
|
+
"delete_override",
|
|
115
|
+
"clear_overrides",
|
|
116
|
+
"run_with_active_session",
|
|
117
|
+
"track_active_session",
|
|
118
|
+
"validate_name",
|
|
119
|
+
# Config
|
|
120
|
+
"DEFAULT_PROXY_TEMPLATE",
|
|
121
|
+
"DEFAULT_PROXY_BASE_URL",
|
|
122
|
+
"LAUNCH_MODE_HOST",
|
|
123
|
+
"LAUNCH_MODE_SIDECAR",
|
|
124
|
+
"SIDECAR_RUNTIME_BASE_URL",
|
|
125
|
+
# Exceptions (base + common operational)
|
|
126
|
+
"ForgeSessionError",
|
|
127
|
+
"SessionNotFoundError",
|
|
128
|
+
"SessionExistsError",
|
|
129
|
+
"InvalidSessionNameError",
|
|
130
|
+
]
|