gobby 0.2.5__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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sessions package for multi-CLI session management.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
- SessionManager: Session registration, handoff, and context restoration
|
|
6
|
+
- SummaryFileGenerator: LLM-powered session summaries (failover)
|
|
7
|
+
- Transcript parsers: CLI-specific transcript parsing (Claude, Codex, Gemini, etc.)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from gobby.sessions.manager import SessionManager
|
|
11
|
+
from gobby.sessions.summary import SummaryFileGenerator
|
|
12
|
+
|
|
13
|
+
__all__ = ["SessionManager", "SummaryFileGenerator"]
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transcript analyzer for autonomous session handoff.
|
|
3
|
+
|
|
4
|
+
Extracts structured context from session transcripts to support
|
|
5
|
+
autonomous continuity without relying on manual /clear boundaries.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from gobby.sessions.transcripts.base import TranscriptParser
|
|
16
|
+
from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class HandoffContext:
|
|
23
|
+
"""Structured context for autonomous handoff."""
|
|
24
|
+
|
|
25
|
+
active_gobby_task: dict[str, Any] | None = None
|
|
26
|
+
todo_state: list[dict[str, Any]] = field(default_factory=list)
|
|
27
|
+
files_modified: list[str] = field(default_factory=list)
|
|
28
|
+
git_commits: list[dict[str, Any]] = field(default_factory=list)
|
|
29
|
+
git_status: str = ""
|
|
30
|
+
initial_goal: str = ""
|
|
31
|
+
recent_activity: list[str] = field(default_factory=list)
|
|
32
|
+
key_decisions: list[str] | None = None
|
|
33
|
+
active_worktree: dict[str, Any] | None = None
|
|
34
|
+
"""Worktree context if session is operating in a worktree."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TranscriptAnalyzer:
|
|
38
|
+
"""
|
|
39
|
+
Transcript analysis for handoff context.
|
|
40
|
+
|
|
41
|
+
Primary: Claude Code
|
|
42
|
+
Extensible: Other CLIs via TranscriptParser protocol
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, parser: TranscriptParser | None = None):
|
|
46
|
+
"""
|
|
47
|
+
Initialize TranscriptAnalyzer.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
parser: Optional specific parser. Defaults to ClaudeTranscriptParser.
|
|
51
|
+
"""
|
|
52
|
+
self.parser = parser or ClaudeTranscriptParser()
|
|
53
|
+
|
|
54
|
+
def extract_handoff_context(
|
|
55
|
+
self, turns: list[dict[str, Any]], max_turns: int = 150
|
|
56
|
+
) -> HandoffContext:
|
|
57
|
+
"""
|
|
58
|
+
Extract context for autonomous handoff.
|
|
59
|
+
|
|
60
|
+
Analyzes recent turns to find:
|
|
61
|
+
- Active task state from gobby-tasks calls
|
|
62
|
+
- TodoWrite state from Claude's internal tracking (if available in transcript)
|
|
63
|
+
- Files modified from Edit/Write/Bash calls
|
|
64
|
+
- Git commits from Bash calls
|
|
65
|
+
- The original user goal (first user message)
|
|
66
|
+
- Recent tool activity summaries
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
turns: List of transcript turns (dicts)
|
|
70
|
+
max_turns: Maximum number of turns to look back for context
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
HandoffContext object populated with extracted data
|
|
74
|
+
"""
|
|
75
|
+
context = HandoffContext()
|
|
76
|
+
|
|
77
|
+
if not turns:
|
|
78
|
+
return context
|
|
79
|
+
|
|
80
|
+
# 1. Extract Initial Goal (First User Message)
|
|
81
|
+
# We scan from the beginning to find the first user message
|
|
82
|
+
for turn in turns:
|
|
83
|
+
if turn.get("type") == "user":
|
|
84
|
+
msg = turn.get("message", {})
|
|
85
|
+
context.initial_goal = str(msg.get("content", "")).strip()
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
# 2. Analyze Recent Activity (Scan backwards)
|
|
89
|
+
# We look at the last `max_turns` or less
|
|
90
|
+
relevant_turns = turns[-max_turns:] if len(turns) > max_turns else turns
|
|
91
|
+
|
|
92
|
+
# Track what we've found to avoid duplicates where appropriate
|
|
93
|
+
found_active_task = False
|
|
94
|
+
modified_files_set: set[str] = set()
|
|
95
|
+
|
|
96
|
+
for turn in reversed(relevant_turns):
|
|
97
|
+
message = turn.get("message", {})
|
|
98
|
+
content_blocks = message.get("content", [])
|
|
99
|
+
|
|
100
|
+
# Handle Claude's content block list format
|
|
101
|
+
if isinstance(content_blocks, list):
|
|
102
|
+
for block in content_blocks:
|
|
103
|
+
if not isinstance(block, dict):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
block_type = block.get("type")
|
|
107
|
+
|
|
108
|
+
# Check for Tool Use
|
|
109
|
+
if block_type == "tool_use":
|
|
110
|
+
self._analyze_tool_use(
|
|
111
|
+
block, context, found_active_task, modified_files_set
|
|
112
|
+
)
|
|
113
|
+
if (
|
|
114
|
+
block.get("name") == "mcp_call_tool"
|
|
115
|
+
and block.get("input", {}).get("server_name") == "gobby-tasks"
|
|
116
|
+
):
|
|
117
|
+
# We found a task interaction, but we want the *latest* active one
|
|
118
|
+
# The helper _analyze_tool_use will handle extraction,
|
|
119
|
+
# we just mark we found some task activity if needed.
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
context.files_modified = sorted(modified_files_set)
|
|
123
|
+
|
|
124
|
+
# 3. Extract TodoWrite state
|
|
125
|
+
context.todo_state = self._extract_todowrite(relevant_turns)
|
|
126
|
+
|
|
127
|
+
# 4. Recent Activity Summary (Last 10 calls)
|
|
128
|
+
# Extract meaningful details from recent tool uses
|
|
129
|
+
recent_tools = []
|
|
130
|
+
count = 0
|
|
131
|
+
for turn in reversed(turns):
|
|
132
|
+
if count >= 10:
|
|
133
|
+
break
|
|
134
|
+
message = turn.get("message", {})
|
|
135
|
+
content = message.get("content", [])
|
|
136
|
+
if isinstance(content, list):
|
|
137
|
+
for block in content:
|
|
138
|
+
if isinstance(block, dict) and block.get("type") == "tool_use":
|
|
139
|
+
description = self._format_tool_description(block)
|
|
140
|
+
recent_tools.append(description)
|
|
141
|
+
count += 1
|
|
142
|
+
if count >= 10:
|
|
143
|
+
break
|
|
144
|
+
context.recent_activity = recent_tools
|
|
145
|
+
|
|
146
|
+
return context
|
|
147
|
+
|
|
148
|
+
def _analyze_tool_use(
|
|
149
|
+
self,
|
|
150
|
+
block: dict[str, Any],
|
|
151
|
+
context: HandoffContext,
|
|
152
|
+
found_active_task: bool,
|
|
153
|
+
modified_files_set: set[str],
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Helper to analyze a single tool use block."""
|
|
156
|
+
tool_name = block.get("name")
|
|
157
|
+
tool_input = block.get("input", {})
|
|
158
|
+
|
|
159
|
+
# -- Gobby Tasks --
|
|
160
|
+
if tool_name == "mcp_call_tool":
|
|
161
|
+
server = tool_input.get("server_name")
|
|
162
|
+
tool = tool_input.get("tool_name")
|
|
163
|
+
args = tool_input.get("arguments", {})
|
|
164
|
+
|
|
165
|
+
if server == "gobby-tasks":
|
|
166
|
+
# We want the most recent task interaction that implies working on a task
|
|
167
|
+
# e.g., create_task, update_task, get_task
|
|
168
|
+
if not context.active_gobby_task:
|
|
169
|
+
# Heuristic: If we see a task interaction, it might be the active task
|
|
170
|
+
# especially if it's get_task or update_task
|
|
171
|
+
task_id = args.get("task_id") or args.get("id")
|
|
172
|
+
if task_id:
|
|
173
|
+
context.active_gobby_task = {
|
|
174
|
+
"id": task_id,
|
|
175
|
+
"action": tool,
|
|
176
|
+
# We don't have the full task object here, just the ID and intent
|
|
177
|
+
# The injection template might need to fetch it or we assume
|
|
178
|
+
# the ID is enough for the user to know.
|
|
179
|
+
# Ideally, we'd have the title, but we can't get it from the tool input easily
|
|
180
|
+
# unless it was a create/update with title.
|
|
181
|
+
# For now, store what we have.
|
|
182
|
+
"title": args.get("title", f"Task {task_id}"),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# -- File Modifications --
|
|
186
|
+
elif tool_name in ("Edit", "Write", "Replace", "replace_file_content", "write_to_file"):
|
|
187
|
+
# Claude Code uses Edit/Write? Antigravity uses write_to_file/replace_file_content
|
|
188
|
+
# We should support both if possible or stick to what we expect Claude to use.
|
|
189
|
+
# Claude Code typically uses `grep_search`, `view_file`, `edit_file`?
|
|
190
|
+
# Let's assume standard names or generic ones.
|
|
191
|
+
path = (
|
|
192
|
+
tool_input.get("file_path")
|
|
193
|
+
or tool_input.get("TargetFile")
|
|
194
|
+
or tool_input.get("path")
|
|
195
|
+
)
|
|
196
|
+
if path:
|
|
197
|
+
modified_files_set.add(path)
|
|
198
|
+
|
|
199
|
+
# -- Git Commits --
|
|
200
|
+
elif tool_name == "Bash":
|
|
201
|
+
command = tool_input.get("command", "")
|
|
202
|
+
if "git commit" in command:
|
|
203
|
+
# Attempt to extract message
|
|
204
|
+
# This is a bit brittle, but useful context
|
|
205
|
+
context.git_commits.append(
|
|
206
|
+
{
|
|
207
|
+
"command": command,
|
|
208
|
+
"timestamp": datetime.now().isoformat(), # Approx time
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _format_tool_description(self, block: dict[str, Any]) -> str:
|
|
213
|
+
"""
|
|
214
|
+
Format a tool use block into a human-readable description.
|
|
215
|
+
|
|
216
|
+
Extracts meaningful details instead of just showing the tool name.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
block: Tool use block with 'name' and 'input' keys
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Human-readable description of what the tool call did
|
|
223
|
+
"""
|
|
224
|
+
tool_name = block.get("name", "unknown")
|
|
225
|
+
tool_input = block.get("input", {})
|
|
226
|
+
|
|
227
|
+
# MCP tool calls - show server.tool
|
|
228
|
+
if tool_name in ("mcp__gobby__call_tool", "mcp_call_tool"):
|
|
229
|
+
server = tool_input.get("server_name", "unknown")
|
|
230
|
+
tool = tool_input.get("tool_name", "unknown")
|
|
231
|
+
return f"Called {server}.{tool}"
|
|
232
|
+
|
|
233
|
+
# Bash - show the command (truncated)
|
|
234
|
+
if tool_name == "Bash":
|
|
235
|
+
command = tool_input.get("command", "")
|
|
236
|
+
# Truncate long commands
|
|
237
|
+
if len(command) > 60:
|
|
238
|
+
command = command[:57] + "..."
|
|
239
|
+
return f"Ran: {command}"
|
|
240
|
+
|
|
241
|
+
# Edit/Write - show the file path
|
|
242
|
+
if tool_name in ("Edit", "Write"):
|
|
243
|
+
path = tool_input.get("file_path", "")
|
|
244
|
+
if path:
|
|
245
|
+
return f"{tool_name}: {path}"
|
|
246
|
+
return f"Called {tool_name}"
|
|
247
|
+
|
|
248
|
+
# Read - show the file path
|
|
249
|
+
if tool_name == "Read":
|
|
250
|
+
path = tool_input.get("file_path", "")
|
|
251
|
+
if path:
|
|
252
|
+
return f"Read: {path}"
|
|
253
|
+
return "Called Read"
|
|
254
|
+
|
|
255
|
+
# Glob - show the pattern
|
|
256
|
+
if tool_name == "Glob":
|
|
257
|
+
pattern = tool_input.get("pattern", "")
|
|
258
|
+
if pattern:
|
|
259
|
+
return f"Glob: {pattern}"
|
|
260
|
+
return "Called Glob"
|
|
261
|
+
|
|
262
|
+
# Grep - show the pattern
|
|
263
|
+
if tool_name == "Grep":
|
|
264
|
+
pattern = tool_input.get("pattern", "")
|
|
265
|
+
if pattern:
|
|
266
|
+
# Truncate long patterns
|
|
267
|
+
if len(pattern) > 40:
|
|
268
|
+
pattern = pattern[:37] + "..."
|
|
269
|
+
return f"Grep: {pattern}"
|
|
270
|
+
return "Called Grep"
|
|
271
|
+
|
|
272
|
+
# TodoWrite - show count
|
|
273
|
+
if tool_name == "TodoWrite":
|
|
274
|
+
todos = tool_input.get("todos", [])
|
|
275
|
+
return f"TodoWrite: {len(todos)} items"
|
|
276
|
+
|
|
277
|
+
# Task tool - show subagent type
|
|
278
|
+
if tool_name == "Task":
|
|
279
|
+
subagent = tool_input.get("subagent_type", "")
|
|
280
|
+
desc = tool_input.get("description", "")
|
|
281
|
+
if subagent:
|
|
282
|
+
return f"Task ({subagent}): {desc}" if desc else f"Task ({subagent})"
|
|
283
|
+
return f"Task: {desc}" if desc else "Called Task"
|
|
284
|
+
|
|
285
|
+
# Default - just show the tool name
|
|
286
|
+
return f"Called {tool_name}"
|
|
287
|
+
|
|
288
|
+
def _extract_todowrite(self, turns: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
289
|
+
"""
|
|
290
|
+
Extract the most recent TodoWrite state from transcript.
|
|
291
|
+
|
|
292
|
+
Scans turns in reverse to find the last TodoWrite tool call and
|
|
293
|
+
extracts the todos list.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
turns: List of transcript turns to scan
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List of todo dicts with 'content' and 'status' keys, or empty list
|
|
300
|
+
"""
|
|
301
|
+
for turn in reversed(turns):
|
|
302
|
+
message = turn.get("message", {})
|
|
303
|
+
content = message.get("content", [])
|
|
304
|
+
|
|
305
|
+
if isinstance(content, list):
|
|
306
|
+
for block in content:
|
|
307
|
+
if isinstance(block, dict) and block.get("type") == "tool_use":
|
|
308
|
+
if block.get("name") == "TodoWrite":
|
|
309
|
+
tool_input = block.get("input", {})
|
|
310
|
+
todos = tool_input.get("todos", [])
|
|
311
|
+
|
|
312
|
+
if todos:
|
|
313
|
+
# Return the raw todo list for HandoffContext
|
|
314
|
+
return [
|
|
315
|
+
{
|
|
316
|
+
"content": todo.get("content", ""),
|
|
317
|
+
"status": todo.get("status", "pending"),
|
|
318
|
+
}
|
|
319
|
+
for todo in todos
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
return []
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session lifecycle manager.
|
|
3
|
+
|
|
4
|
+
Handles background jobs for:
|
|
5
|
+
- Expiring stale sessions
|
|
6
|
+
- Processing transcripts for expired sessions
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from gobby.config.app import SessionLifecycleConfig
|
|
15
|
+
from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
|
|
16
|
+
from gobby.sessions.transcripts.codex import CodexTranscriptParser
|
|
17
|
+
from gobby.sessions.transcripts.gemini import GeminiTranscriptParser
|
|
18
|
+
from gobby.storage.database import DatabaseProtocol
|
|
19
|
+
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
20
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SessionLifecycleManager:
|
|
26
|
+
"""
|
|
27
|
+
Manages session lifecycle background jobs.
|
|
28
|
+
|
|
29
|
+
Two independent jobs:
|
|
30
|
+
1. expire_stale_sessions - marks old active/paused sessions as expired
|
|
31
|
+
2. process_pending_transcripts - processes transcripts for expired sessions
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, db: DatabaseProtocol, config: SessionLifecycleConfig):
|
|
35
|
+
self.db = db
|
|
36
|
+
self.config = config
|
|
37
|
+
self.session_manager = LocalSessionManager(db)
|
|
38
|
+
self.message_manager = LocalSessionMessageManager(db)
|
|
39
|
+
|
|
40
|
+
self._running = False
|
|
41
|
+
self._expire_task: asyncio.Task[None] | None = None
|
|
42
|
+
self._process_task: asyncio.Task[None] | None = None
|
|
43
|
+
|
|
44
|
+
async def start(self) -> None:
|
|
45
|
+
"""Start background jobs."""
|
|
46
|
+
if self._running:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
self._running = True
|
|
50
|
+
|
|
51
|
+
# Start expire job
|
|
52
|
+
self._expire_task = asyncio.create_task(
|
|
53
|
+
self._expire_loop(),
|
|
54
|
+
name="session-lifecycle-expire",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Start process job
|
|
58
|
+
self._process_task = asyncio.create_task(
|
|
59
|
+
self._process_loop(),
|
|
60
|
+
name="session-lifecycle-process",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
logger.info(
|
|
64
|
+
f"SessionLifecycleManager started "
|
|
65
|
+
f"(expire every {self.config.expire_check_interval_minutes}m, "
|
|
66
|
+
f"process every {self.config.transcript_processing_interval_minutes}m)"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async def stop(self) -> None:
|
|
70
|
+
"""Stop background jobs."""
|
|
71
|
+
self._running = False
|
|
72
|
+
|
|
73
|
+
tasks = [t for t in [self._expire_task, self._process_task] if t]
|
|
74
|
+
for task in tasks:
|
|
75
|
+
task.cancel()
|
|
76
|
+
|
|
77
|
+
if tasks:
|
|
78
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
79
|
+
|
|
80
|
+
self._expire_task = None
|
|
81
|
+
self._process_task = None
|
|
82
|
+
|
|
83
|
+
logger.info("SessionLifecycleManager stopped")
|
|
84
|
+
|
|
85
|
+
async def _expire_loop(self) -> None:
|
|
86
|
+
"""Background loop for expiring stale sessions."""
|
|
87
|
+
interval_seconds = self.config.expire_check_interval_minutes * 60
|
|
88
|
+
|
|
89
|
+
while self._running:
|
|
90
|
+
try:
|
|
91
|
+
await self._expire_stale_sessions()
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Error in expire loop: {e}")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
await asyncio.sleep(interval_seconds)
|
|
97
|
+
except asyncio.CancelledError:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
async def _process_loop(self) -> None:
|
|
101
|
+
"""Background loop for processing pending transcripts."""
|
|
102
|
+
interval_seconds = self.config.transcript_processing_interval_minutes * 60
|
|
103
|
+
|
|
104
|
+
while self._running:
|
|
105
|
+
try:
|
|
106
|
+
await self._process_pending_transcripts()
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Error in process loop: {e}")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
await asyncio.sleep(interval_seconds)
|
|
112
|
+
except asyncio.CancelledError:
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
async def _expire_stale_sessions(self) -> int:
|
|
116
|
+
"""Pause inactive active sessions and expire stale sessions."""
|
|
117
|
+
# First, pause active sessions that have been idle too long
|
|
118
|
+
# This catches orphaned sessions that never got AFTER_AGENT hook
|
|
119
|
+
paused = self.session_manager.pause_inactive_active_sessions(
|
|
120
|
+
timeout_minutes=self.config.active_session_pause_minutes
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Then expire sessions that have been paused/active for too long
|
|
124
|
+
expired = self.session_manager.expire_stale_sessions(
|
|
125
|
+
timeout_hours=self.config.stale_session_timeout_hours
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return paused + expired
|
|
129
|
+
|
|
130
|
+
async def _process_pending_transcripts(self) -> int:
|
|
131
|
+
"""Process transcripts for expired sessions."""
|
|
132
|
+
sessions = self.session_manager.get_pending_transcript_sessions(
|
|
133
|
+
limit=self.config.transcript_processing_batch_size
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not sessions:
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
processed = 0
|
|
140
|
+
for session in sessions:
|
|
141
|
+
try:
|
|
142
|
+
await self._process_session_transcript(session.id, session.jsonl_path)
|
|
143
|
+
self.session_manager.mark_transcript_processed(session.id)
|
|
144
|
+
processed += 1
|
|
145
|
+
logger.debug(f"Processed transcript for session {session.id}")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Failed to process transcript for {session.id}: {e}")
|
|
148
|
+
|
|
149
|
+
if processed > 0:
|
|
150
|
+
logger.info(f"Processed {processed} session transcripts")
|
|
151
|
+
|
|
152
|
+
return processed
|
|
153
|
+
|
|
154
|
+
async def _process_session_transcript(self, session_id: str, jsonl_path: str | None) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Process a full transcript for a session.
|
|
157
|
+
|
|
158
|
+
Reads the entire transcript and stores messages.
|
|
159
|
+
Aggregates token usage and costs.
|
|
160
|
+
Uses idempotent upsert so re-processing is safe.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
session_id: Session ID
|
|
164
|
+
jsonl_path: Path to transcript JSONL file
|
|
165
|
+
"""
|
|
166
|
+
if not jsonl_path or not os.path.exists(jsonl_path):
|
|
167
|
+
logger.warning(f"Transcript not found for session {session_id}: {jsonl_path}")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Read entire file
|
|
171
|
+
try:
|
|
172
|
+
with open(jsonl_path, encoding="utf-8") as f:
|
|
173
|
+
lines = f.readlines()
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f"Error reading transcript {jsonl_path}: {e}")
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
if not lines:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Parse all lines
|
|
182
|
+
session = self.session_manager.get(session_id)
|
|
183
|
+
if not session:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# Choose parser based on source
|
|
187
|
+
# Default to Claude for backward compatibility or safety
|
|
188
|
+
# But we should rely on session.source if possible
|
|
189
|
+
parser: Any = ClaudeTranscriptParser()
|
|
190
|
+
if session.source == "gemini":
|
|
191
|
+
parser = GeminiTranscriptParser()
|
|
192
|
+
elif session.source == "codex":
|
|
193
|
+
parser = CodexTranscriptParser()
|
|
194
|
+
elif session.source == "antigravity":
|
|
195
|
+
parser = ClaudeTranscriptParser()
|
|
196
|
+
# Default (claude or unknown) uses Claude transcript format
|
|
197
|
+
|
|
198
|
+
messages = parser.parse_lines(lines, start_index=0)
|
|
199
|
+
|
|
200
|
+
if not messages:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Store messages (upsert - safe for re-processing)
|
|
204
|
+
await self.message_manager.store_messages(session_id, messages)
|
|
205
|
+
|
|
206
|
+
# Aggregate usage
|
|
207
|
+
input_tokens = 0
|
|
208
|
+
output_tokens = 0
|
|
209
|
+
cache_creation_tokens = 0
|
|
210
|
+
cache_read_tokens = 0
|
|
211
|
+
total_cost_usd = 0.0
|
|
212
|
+
|
|
213
|
+
for msg in messages:
|
|
214
|
+
if msg.usage:
|
|
215
|
+
input_tokens += msg.usage.input_tokens
|
|
216
|
+
output_tokens += msg.usage.output_tokens
|
|
217
|
+
cache_creation_tokens += msg.usage.cache_creation_tokens
|
|
218
|
+
cache_read_tokens += msg.usage.cache_read_tokens
|
|
219
|
+
if msg.usage.total_cost_usd:
|
|
220
|
+
total_cost_usd += msg.usage.total_cost_usd
|
|
221
|
+
|
|
222
|
+
# Update session with aggregated usage
|
|
223
|
+
# We only update if we found some usage, to avoid overwriting with zeros if re-processing
|
|
224
|
+
# (though re-processing from scratch IS the source of truth, so zeros might be correct if no usage found)
|
|
225
|
+
# Actually, let's always update to ensure consistency with the file
|
|
226
|
+
self.session_manager.update_usage(
|
|
227
|
+
session_id=session_id,
|
|
228
|
+
input_tokens=input_tokens,
|
|
229
|
+
output_tokens=output_tokens,
|
|
230
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
231
|
+
cache_read_tokens=cache_read_tokens,
|
|
232
|
+
total_cost_usd=total_cost_usd,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Update processing state
|
|
236
|
+
await self.message_manager.update_state(
|
|
237
|
+
session_id=session_id,
|
|
238
|
+
byte_offset=sum(len(line.encode("utf-8")) for line in lines),
|
|
239
|
+
message_index=messages[-1].index,
|
|
240
|
+
)
|