voidx 1.0.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.
- voidx/__init__.py +3 -0
- voidx/agent/__init__.py +0 -0
- voidx/agent/agents.py +439 -0
- voidx/agent/attachments.py +235 -0
- voidx/agent/graph.py +463 -0
- voidx/agent/graph_components/__init__.py +1 -0
- voidx/agent/graph_components/compaction.py +268 -0
- voidx/agent/graph_components/permissions.py +139 -0
- voidx/agent/graph_components/run_loop.py +532 -0
- voidx/agent/graph_components/runtime.py +14 -0
- voidx/agent/graph_components/streaming.py +351 -0
- voidx/agent/graph_components/subagent.py +278 -0
- voidx/agent/graph_components/tool_execution.py +208 -0
- voidx/agent/runtime_context.py +368 -0
- voidx/agent/slash.py +466 -0
- voidx/agent/slash_components/__init__.py +1 -0
- voidx/agent/slash_components/code_ide.py +68 -0
- voidx/agent/slash_components/lsp.py +105 -0
- voidx/agent/slash_components/mcp.py +332 -0
- voidx/agent/slash_components/model.py +419 -0
- voidx/agent/slash_components/runtime.py +55 -0
- voidx/agent/slash_components/skills.py +94 -0
- voidx/agent/state.py +32 -0
- voidx/agent/task_state.py +278 -0
- voidx/agent/tool_filters.py +27 -0
- voidx/config.py +707 -0
- voidx/llm/__init__.py +0 -0
- voidx/llm/catalog.py +188 -0
- voidx/llm/compaction.py +267 -0
- voidx/llm/context.py +43 -0
- voidx/llm/instruction.py +220 -0
- voidx/llm/provider.py +312 -0
- voidx/llm/usage.py +341 -0
- voidx/lsp/__init__.py +30 -0
- voidx/lsp/client.py +259 -0
- voidx/lsp/config.py +172 -0
- voidx/lsp/detector.py +512 -0
- voidx/lsp/errors.py +19 -0
- voidx/lsp/manager.py +280 -0
- voidx/lsp/schema.py +179 -0
- voidx/lsp/service.py +103 -0
- voidx/main.py +154 -0
- voidx/mcp/__init__.py +33 -0
- voidx/mcp/client.py +458 -0
- voidx/mcp/manager.py +267 -0
- voidx/mcp/schema.py +112 -0
- voidx/mcp/tool.py +122 -0
- voidx/mcp_servers/__init__.py +1 -0
- voidx/mcp_servers/web.py +104 -0
- voidx/memory/__init__.py +0 -0
- voidx/memory/context_frames.py +188 -0
- voidx/memory/model_profiles.py +98 -0
- voidx/memory/runtime_state.py +240 -0
- voidx/memory/session.py +272 -0
- voidx/memory/store.py +245 -0
- voidx/memory/transcript.py +137 -0
- voidx/permission/__init__.py +28 -0
- voidx/permission/engine.py +430 -0
- voidx/permission/evaluate.py +114 -0
- voidx/permission/sandbox.py +280 -0
- voidx/permission/schema.py +24 -0
- voidx/permission/service.py +314 -0
- voidx/permission/wildcard.py +34 -0
- voidx/skills/__init__.py +18 -0
- voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
- voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
- voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
- voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
- voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
- voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
- voidx/skills/policy.py +97 -0
- voidx/skills/registry.py +162 -0
- voidx/skills/schema.py +47 -0
- voidx/skills/service.py +199 -0
- voidx/tools/__init__.py +0 -0
- voidx/tools/agent.py +81 -0
- voidx/tools/base.py +86 -0
- voidx/tools/bash.py +105 -0
- voidx/tools/file_ops.py +193 -0
- voidx/tools/lsp.py +155 -0
- voidx/tools/registry.py +104 -0
- voidx/tools/repomap.py +238 -0
- voidx/tools/search.py +162 -0
- voidx/tools/task_status.py +57 -0
- voidx/tools/task_tracker.py +81 -0
- voidx/tools/todo.py +82 -0
- voidx/tools/web_content.py +357 -0
- voidx/tools/web_mcp.py +107 -0
- voidx/tools/webfetch.py +155 -0
- voidx/tools/websearch.py +276 -0
- voidx/ui/__init__.py +0 -0
- voidx/ui/app.py +1033 -0
- voidx/ui/app_components/__init__.py +1 -0
- voidx/ui/app_components/clipboard_image.py +245 -0
- voidx/ui/app_components/commands.py +18 -0
- voidx/ui/app_components/controls.py +29 -0
- voidx/ui/app_components/file_picker.py +115 -0
- voidx/ui/app_components/formatting.py +187 -0
- voidx/ui/app_components/git_changes.py +51 -0
- voidx/ui/app_components/rendering.py +1169 -0
- voidx/ui/browse.py +160 -0
- voidx/ui/capture.py +169 -0
- voidx/ui/code_ide.py +251 -0
- voidx/ui/commands.py +83 -0
- voidx/ui/console.py +381 -0
- voidx/ui/console_components/__init__.py +1 -0
- voidx/ui/console_components/formatting.py +96 -0
- voidx/ui/console_components/streaming.py +253 -0
- voidx/ui/diff.py +331 -0
- voidx/ui/dock.py +372 -0
- voidx/ui/dock_components/__init__.py +1 -0
- voidx/ui/dock_components/formatting.py +123 -0
- voidx/ui/dock_components/nodes.py +401 -0
- voidx/ui/dock_components/state.py +51 -0
- voidx/ui/event_components/__init__.py +1 -0
- voidx/ui/event_components/schema.py +249 -0
- voidx/ui/events.py +341 -0
- voidx/ui/session_changes.py +163 -0
- voidx/ui/startup.py +161 -0
- voidx/ui/transcript.py +148 -0
- voidx/ui/tree.py +316 -0
- voidx-1.0.0.dist-info/METADATA +59 -0
- voidx-1.0.0.dist-info/RECORD +126 -0
- voidx-1.0.0.dist-info/WHEEL +5 -0
- voidx-1.0.0.dist-info/entry_points.txt +2 -0
- voidx-1.0.0.dist-info/top_level.txt +1 -0
voidx/memory/session.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Session manager — CRUD, message persistence, auto-titling.
|
|
2
|
+
|
|
3
|
+
Inspired by opencode's session system: typed Info, create/fork/remove,
|
|
4
|
+
timestamp-based listing, message hydration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from voidx.memory.store import _execute_commit, _fetch_all, _fetch_one
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _now() -> str:
|
|
19
|
+
return datetime.now(timezone.utc).isoformat()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _uid() -> str:
|
|
23
|
+
return uuid.uuid4().hex[:12]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── models ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
class SessionInfo(BaseModel):
|
|
29
|
+
id: str
|
|
30
|
+
title: str = "New session"
|
|
31
|
+
workspace: str = "."
|
|
32
|
+
model_provider: str = "anthropic"
|
|
33
|
+
model_name: str = "claude-sonnet-4-6"
|
|
34
|
+
created_at: str = Field(default_factory=_now)
|
|
35
|
+
updated_at: str = Field(default_factory=_now)
|
|
36
|
+
message_count: int = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MessageRow(BaseModel):
|
|
40
|
+
id: int | None = None # auto-increment
|
|
41
|
+
session_id: str
|
|
42
|
+
role: str # system | user | assistant | tool
|
|
43
|
+
content: str = ""
|
|
44
|
+
content_format: str = "text" # "text" | "structured" (e.g. DeepSeek thinking blocks)
|
|
45
|
+
tool_calls: list[dict] | None = None
|
|
46
|
+
tool_call_id: str | None = None
|
|
47
|
+
created_at: str = Field(default_factory=_now)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── session CRUD ────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
async def create_session(
|
|
53
|
+
workspace: str = ".",
|
|
54
|
+
provider: str = "anthropic",
|
|
55
|
+
model: str = "claude-sonnet-4-6",
|
|
56
|
+
) -> SessionInfo:
|
|
57
|
+
sid = _uid()
|
|
58
|
+
now = _now()
|
|
59
|
+
await _execute_commit(
|
|
60
|
+
"""INSERT INTO sessions (id, title, workspace, model_provider, model_name, created_at, updated_at)
|
|
61
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
62
|
+
(sid, "New session", workspace, provider, model, now, now),
|
|
63
|
+
)
|
|
64
|
+
return SessionInfo(
|
|
65
|
+
id=sid, workspace=workspace, model_provider=provider,
|
|
66
|
+
model_name=model, created_at=now, updated_at=now,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def get_session(session_id: str) -> SessionInfo | None:
|
|
71
|
+
row = await _fetch_one(
|
|
72
|
+
"SELECT * FROM sessions WHERE id = ?", (session_id,)
|
|
73
|
+
)
|
|
74
|
+
if not row:
|
|
75
|
+
return None
|
|
76
|
+
count_row = await _fetch_one(
|
|
77
|
+
"SELECT COUNT(*) as cnt FROM messages WHERE session_id = ?",
|
|
78
|
+
(session_id,),
|
|
79
|
+
)
|
|
80
|
+
return SessionInfo(
|
|
81
|
+
id=row["id"], title=row["title"], workspace=row["workspace"],
|
|
82
|
+
model_provider=row["model_provider"], model_name=row["model_name"],
|
|
83
|
+
created_at=row["created_at"], updated_at=row["updated_at"],
|
|
84
|
+
message_count=count_row["cnt"] if count_row else 0,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def list_sessions(limit: int = 50) -> list[SessionInfo]:
|
|
89
|
+
rows = await _fetch_all(
|
|
90
|
+
"""SELECT s.*, COUNT(m.id) as cnt
|
|
91
|
+
FROM sessions s
|
|
92
|
+
LEFT JOIN messages m ON s.id = m.session_id
|
|
93
|
+
GROUP BY s.id
|
|
94
|
+
ORDER BY s.updated_at DESC
|
|
95
|
+
LIMIT ?""",
|
|
96
|
+
(limit,),
|
|
97
|
+
)
|
|
98
|
+
return [
|
|
99
|
+
SessionInfo(
|
|
100
|
+
id=row["id"], title=row["title"], workspace=row["workspace"],
|
|
101
|
+
model_provider=row["model_provider"], model_name=row["model_name"],
|
|
102
|
+
created_at=row["created_at"], updated_at=row["updated_at"],
|
|
103
|
+
message_count=row["cnt"] or 0,
|
|
104
|
+
)
|
|
105
|
+
for row in rows
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def latest_session_for_workspace(workspace: str) -> SessionInfo | None:
|
|
110
|
+
row = await _fetch_one(
|
|
111
|
+
"""SELECT s.*, COUNT(m.id) as cnt
|
|
112
|
+
FROM sessions s
|
|
113
|
+
LEFT JOIN messages m ON s.id = m.session_id
|
|
114
|
+
WHERE s.workspace = ?
|
|
115
|
+
GROUP BY s.id
|
|
116
|
+
ORDER BY s.updated_at DESC
|
|
117
|
+
LIMIT 1""",
|
|
118
|
+
(workspace,),
|
|
119
|
+
)
|
|
120
|
+
if not row:
|
|
121
|
+
return None
|
|
122
|
+
return SessionInfo(
|
|
123
|
+
id=row["id"], title=row["title"], workspace=row["workspace"],
|
|
124
|
+
model_provider=row["model_provider"], model_name=row["model_name"],
|
|
125
|
+
created_at=row["created_at"], updated_at=row["updated_at"],
|
|
126
|
+
message_count=row["cnt"] or 0,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def update_title(session_id: str, title: str) -> None:
|
|
131
|
+
await _execute_commit(
|
|
132
|
+
"UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?",
|
|
133
|
+
(title, _now(), session_id),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def update_session_model(session_id: str, provider: str, model: str) -> None:
|
|
138
|
+
await _execute_commit(
|
|
139
|
+
"UPDATE sessions SET model_provider = ?, model_name = ?, updated_at = ? WHERE id = ?",
|
|
140
|
+
(provider, model, _now(), session_id),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def touch_session(session_id: str) -> None:
|
|
145
|
+
await _execute_commit(
|
|
146
|
+
"UPDATE sessions SET updated_at = ? WHERE id = ?",
|
|
147
|
+
(_now(), session_id),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def delete_session(session_id: str) -> None:
|
|
152
|
+
await _execute_commit("DELETE FROM context_frames WHERE session_id = ?", (session_id,))
|
|
153
|
+
await _execute_commit("DELETE FROM message_runtime_snapshots WHERE session_id = ?", (session_id,))
|
|
154
|
+
await _execute_commit("DELETE FROM session_task_runs WHERE session_id = ?", (session_id,))
|
|
155
|
+
await _execute_commit("DELETE FROM session_runtime_state WHERE session_id = ?", (session_id,))
|
|
156
|
+
await _execute_commit("DELETE FROM transcript_nodes WHERE session_id = ?", (session_id,))
|
|
157
|
+
await _execute_commit("DELETE FROM turns WHERE session_id = ?", (session_id,))
|
|
158
|
+
await _execute_commit("DELETE FROM messages WHERE session_id = ?", (session_id,))
|
|
159
|
+
await _execute_commit("DELETE FROM sessions WHERE id = ?", (session_id,))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ── message persistence ─────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
async def save_message(msg: MessageRow) -> int:
|
|
165
|
+
"""Save a message row. Returns the auto-generated id."""
|
|
166
|
+
cur = await _execute_commit(
|
|
167
|
+
"""INSERT INTO messages (session_id, role, content, content_format, tool_calls, tool_call_id, created_at)
|
|
168
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
169
|
+
(
|
|
170
|
+
msg.session_id,
|
|
171
|
+
msg.role,
|
|
172
|
+
msg.content,
|
|
173
|
+
msg.content_format,
|
|
174
|
+
json.dumps(msg.tool_calls) if msg.tool_calls else None,
|
|
175
|
+
msg.tool_call_id,
|
|
176
|
+
msg.created_at,
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
return cur.lastrowid
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def load_messages(session_id: str) -> list[MessageRow]:
|
|
183
|
+
"""Load all messages for a session, ordered by id."""
|
|
184
|
+
rows = await _fetch_all(
|
|
185
|
+
"SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC",
|
|
186
|
+
(session_id,),
|
|
187
|
+
)
|
|
188
|
+
return [
|
|
189
|
+
MessageRow(
|
|
190
|
+
id=row["id"],
|
|
191
|
+
session_id=row["session_id"],
|
|
192
|
+
role=row["role"],
|
|
193
|
+
content=row["content"],
|
|
194
|
+
content_format=row["content_format"] if "content_format" in row.keys() else "text",
|
|
195
|
+
tool_calls=json.loads(row["tool_calls"]) if row["tool_calls"] else None,
|
|
196
|
+
tool_call_id=row["tool_call_id"],
|
|
197
|
+
created_at=row["created_at"],
|
|
198
|
+
)
|
|
199
|
+
for row in rows
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def clear_messages(session_id: str) -> None:
|
|
204
|
+
await _execute_commit(
|
|
205
|
+
"DELETE FROM context_frames WHERE session_id = ?", (session_id,)
|
|
206
|
+
)
|
|
207
|
+
await _execute_commit(
|
|
208
|
+
"DELETE FROM message_runtime_snapshots WHERE session_id = ?", (session_id,)
|
|
209
|
+
)
|
|
210
|
+
await _execute_commit(
|
|
211
|
+
"DELETE FROM transcript_nodes WHERE session_id = ?", (session_id,)
|
|
212
|
+
)
|
|
213
|
+
await _execute_commit(
|
|
214
|
+
"DELETE FROM turns WHERE session_id = ?", (session_id,)
|
|
215
|
+
)
|
|
216
|
+
await _execute_commit(
|
|
217
|
+
"DELETE FROM messages WHERE session_id = ?", (session_id,)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def delete_messages_from(session_id: str, first_message_id: int) -> None:
|
|
222
|
+
await _execute_commit(
|
|
223
|
+
"DELETE FROM context_frames WHERE session_id = ? AND user_message_id >= ?",
|
|
224
|
+
(session_id, first_message_id),
|
|
225
|
+
)
|
|
226
|
+
await _execute_commit(
|
|
227
|
+
"DELETE FROM message_runtime_snapshots WHERE session_id = ? AND message_id >= ?",
|
|
228
|
+
(session_id, first_message_id),
|
|
229
|
+
)
|
|
230
|
+
await _execute_commit(
|
|
231
|
+
"DELETE FROM messages WHERE session_id = ? AND id >= ?",
|
|
232
|
+
(session_id, first_message_id),
|
|
233
|
+
)
|
|
234
|
+
await touch_session(session_id)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def delete_messages_through(session_id: str, last_message_id: int) -> None:
|
|
238
|
+
await _execute_commit(
|
|
239
|
+
"DELETE FROM context_frames WHERE session_id = ? AND user_message_id <= ?",
|
|
240
|
+
(session_id, last_message_id),
|
|
241
|
+
)
|
|
242
|
+
await _execute_commit(
|
|
243
|
+
"DELETE FROM message_runtime_snapshots WHERE session_id = ? AND message_id <= ?",
|
|
244
|
+
(session_id, last_message_id),
|
|
245
|
+
)
|
|
246
|
+
await _execute_commit(
|
|
247
|
+
"DELETE FROM messages WHERE session_id = ? AND id <= ?",
|
|
248
|
+
(session_id, last_message_id),
|
|
249
|
+
)
|
|
250
|
+
await touch_session(session_id)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
async def last_messages(session_id: str, n: int = 20) -> list[MessageRow]:
|
|
254
|
+
"""Last N messages for a session."""
|
|
255
|
+
rows = await _fetch_all(
|
|
256
|
+
"SELECT * FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?",
|
|
257
|
+
(session_id, n),
|
|
258
|
+
)
|
|
259
|
+
rows = list(reversed(rows))
|
|
260
|
+
return [
|
|
261
|
+
MessageRow(
|
|
262
|
+
id=row["id"],
|
|
263
|
+
session_id=row["session_id"],
|
|
264
|
+
role=row["role"],
|
|
265
|
+
content=row["content"],
|
|
266
|
+
content_format=row["content_format"] if "content_format" in row.keys() else "text",
|
|
267
|
+
tool_calls=json.loads(row["tool_calls"]) if row["tool_calls"] else None,
|
|
268
|
+
tool_call_id=row["tool_call_id"],
|
|
269
|
+
created_at=row["created_at"],
|
|
270
|
+
)
|
|
271
|
+
for row in rows
|
|
272
|
+
]
|
voidx/memory/store.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""SQLite persistence layer — async-safe via asyncio.to_thread()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import sqlite3
|
|
7
|
+
import threading
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable, TypeVar
|
|
10
|
+
|
|
11
|
+
DATA_DIR = Path.home() / ".voidx"
|
|
12
|
+
|
|
13
|
+
_conn: sqlite3.Connection | None = None
|
|
14
|
+
_init_lock = threading.Lock()
|
|
15
|
+
_write_lock = threading.Lock()
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_db() -> sqlite3.Connection:
|
|
20
|
+
global _conn
|
|
21
|
+
if _conn is not None:
|
|
22
|
+
return _conn
|
|
23
|
+
with _init_lock:
|
|
24
|
+
if _conn is not None:
|
|
25
|
+
return _conn
|
|
26
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
db_path = DATA_DIR / "voidx.db"
|
|
28
|
+
conn = sqlite3.connect(str(db_path), check_same_thread=False)
|
|
29
|
+
conn.row_factory = sqlite3.Row
|
|
30
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
31
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
32
|
+
_init_schema(conn)
|
|
33
|
+
_conn = conn
|
|
34
|
+
return _conn
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _init_schema(conn: sqlite3.Connection) -> None:
|
|
38
|
+
conn.executescript("""
|
|
39
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
title TEXT NOT NULL DEFAULT 'New session',
|
|
42
|
+
workspace TEXT NOT NULL DEFAULT '.',
|
|
43
|
+
model_provider TEXT NOT NULL DEFAULT 'anthropic',
|
|
44
|
+
model_name TEXT NOT NULL DEFAULT 'claude-sonnet-4-6',
|
|
45
|
+
created_at TEXT NOT NULL,
|
|
46
|
+
updated_at TEXT NOT NULL
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
50
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
+
session_id TEXT NOT NULL,
|
|
52
|
+
role TEXT NOT NULL CHECK(role IN ('system', 'user', 'assistant', 'tool')),
|
|
53
|
+
content TEXT NOT NULL DEFAULT '',
|
|
54
|
+
tool_calls TEXT,
|
|
55
|
+
tool_call_id TEXT,
|
|
56
|
+
created_at TEXT NOT NULL,
|
|
57
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session
|
|
61
|
+
ON messages(session_id, id);
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS turns (
|
|
64
|
+
session_id TEXT NOT NULL,
|
|
65
|
+
turn_id INTEGER NOT NULL,
|
|
66
|
+
user_message_id INTEGER,
|
|
67
|
+
created_at TEXT NOT NULL,
|
|
68
|
+
completed_at TEXT,
|
|
69
|
+
PRIMARY KEY (session_id, turn_id),
|
|
70
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
|
|
71
|
+
FOREIGN KEY (user_message_id) REFERENCES messages(id) ON DELETE SET NULL
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS transcript_nodes (
|
|
75
|
+
session_id TEXT NOT NULL,
|
|
76
|
+
turn_id INTEGER NOT NULL,
|
|
77
|
+
node_id INTEGER NOT NULL,
|
|
78
|
+
parent_node_id INTEGER,
|
|
79
|
+
sort_order INTEGER NOT NULL,
|
|
80
|
+
node_type TEXT NOT NULL,
|
|
81
|
+
header TEXT NOT NULL DEFAULT '',
|
|
82
|
+
body_json TEXT NOT NULL DEFAULT '[]',
|
|
83
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
84
|
+
collapsed INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
elapsed REAL,
|
|
86
|
+
message_id INTEGER,
|
|
87
|
+
tool_call_id TEXT,
|
|
88
|
+
agent_run_id TEXT,
|
|
89
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
90
|
+
created_at TEXT NOT NULL,
|
|
91
|
+
updated_at TEXT NOT NULL,
|
|
92
|
+
PRIMARY KEY (session_id, turn_id, node_id),
|
|
93
|
+
FOREIGN KEY (session_id, turn_id) REFERENCES turns(session_id, turn_id) ON DELETE CASCADE,
|
|
94
|
+
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_transcript_nodes_session
|
|
98
|
+
ON transcript_nodes(session_id, turn_id, sort_order);
|
|
99
|
+
|
|
100
|
+
CREATE TABLE IF NOT EXISTS session_runtime_state (
|
|
101
|
+
session_id TEXT PRIMARY KEY,
|
|
102
|
+
interaction_mode TEXT NOT NULL DEFAULT 'auto',
|
|
103
|
+
current_intent TEXT NOT NULL DEFAULT 'chat',
|
|
104
|
+
previous_intent TEXT,
|
|
105
|
+
current_goal TEXT NOT NULL DEFAULT '',
|
|
106
|
+
awaiting_implementation_approval INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
approved_scope TEXT NOT NULL DEFAULT '',
|
|
108
|
+
last_plan_summary TEXT NOT NULL DEFAULT '',
|
|
109
|
+
compaction_summary TEXT NOT NULL DEFAULT '',
|
|
110
|
+
updated_at TEXT NOT NULL,
|
|
111
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
CREATE TABLE IF NOT EXISTS session_task_runs (
|
|
115
|
+
session_id TEXT PRIMARY KEY,
|
|
116
|
+
goal TEXT NOT NULL DEFAULT '',
|
|
117
|
+
phase TEXT NOT NULL DEFAULT 'clarify',
|
|
118
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
119
|
+
approved_scope TEXT NOT NULL DEFAULT '',
|
|
120
|
+
awaiting_implementation_approval INTEGER NOT NULL DEFAULT 0,
|
|
121
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
122
|
+
updated_at TEXT NOT NULL,
|
|
123
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE TABLE IF NOT EXISTS message_runtime_snapshots (
|
|
127
|
+
message_id INTEGER PRIMARY KEY,
|
|
128
|
+
session_id TEXT NOT NULL,
|
|
129
|
+
interaction_mode TEXT NOT NULL DEFAULT 'auto',
|
|
130
|
+
task_intent TEXT NOT NULL DEFAULT 'chat',
|
|
131
|
+
implementation_allowed INTEGER NOT NULL DEFAULT 0,
|
|
132
|
+
intent_resolution_reason TEXT NOT NULL DEFAULT '',
|
|
133
|
+
goal TEXT NOT NULL DEFAULT '',
|
|
134
|
+
goal_phase TEXT NOT NULL DEFAULT 'clarify',
|
|
135
|
+
goal_status TEXT NOT NULL DEFAULT 'idle',
|
|
136
|
+
goal_turn_count INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
awaiting_implementation_approval INTEGER NOT NULL DEFAULT 0,
|
|
138
|
+
approved_scope TEXT NOT NULL DEFAULT '',
|
|
139
|
+
created_at TEXT NOT NULL,
|
|
140
|
+
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
|
|
141
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_message_runtime_snapshots_session
|
|
145
|
+
ON message_runtime_snapshots(session_id, message_id);
|
|
146
|
+
|
|
147
|
+
CREATE TABLE IF NOT EXISTS context_frames (
|
|
148
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
149
|
+
session_id TEXT NOT NULL,
|
|
150
|
+
user_message_id INTEGER,
|
|
151
|
+
frame_kind TEXT NOT NULL DEFAULT 'main',
|
|
152
|
+
agent_role TEXT NOT NULL DEFAULT 'orchestrator',
|
|
153
|
+
provider TEXT NOT NULL,
|
|
154
|
+
model TEXT NOT NULL,
|
|
155
|
+
prefix_hash TEXT NOT NULL,
|
|
156
|
+
frame_hash TEXT NOT NULL,
|
|
157
|
+
message_count INTEGER NOT NULL,
|
|
158
|
+
token_estimate INTEGER NOT NULL DEFAULT 0,
|
|
159
|
+
messages_json TEXT NOT NULL,
|
|
160
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
161
|
+
created_at TEXT NOT NULL,
|
|
162
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
|
|
163
|
+
FOREIGN KEY (user_message_id) REFERENCES messages(id) ON DELETE SET NULL
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_context_frames_session
|
|
167
|
+
ON context_frames(session_id, id);
|
|
168
|
+
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_context_frames_prefix
|
|
170
|
+
ON context_frames(session_id, prefix_hash);
|
|
171
|
+
|
|
172
|
+
CREATE TABLE IF NOT EXISTS model_profiles (
|
|
173
|
+
name TEXT PRIMARY KEY,
|
|
174
|
+
provider TEXT NOT NULL,
|
|
175
|
+
model TEXT NOT NULL,
|
|
176
|
+
api_key TEXT NOT NULL DEFAULT '',
|
|
177
|
+
base_url TEXT,
|
|
178
|
+
protocol TEXT,
|
|
179
|
+
reasoning_effort TEXT,
|
|
180
|
+
created_at TEXT NOT NULL,
|
|
181
|
+
updated_at TEXT NOT NULL
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_model_profiles_provider
|
|
185
|
+
ON model_profiles(provider);
|
|
186
|
+
""")
|
|
187
|
+
# Migration: add content_format column for existing SQLite stores.
|
|
188
|
+
try:
|
|
189
|
+
conn.execute(
|
|
190
|
+
"ALTER TABLE messages ADD COLUMN content_format TEXT NOT NULL DEFAULT 'text'"
|
|
191
|
+
)
|
|
192
|
+
except sqlite3.OperationalError:
|
|
193
|
+
pass
|
|
194
|
+
try:
|
|
195
|
+
conn.execute(
|
|
196
|
+
"ALTER TABLE session_runtime_state ADD COLUMN compaction_summary TEXT NOT NULL DEFAULT ''"
|
|
197
|
+
)
|
|
198
|
+
except sqlite3.OperationalError:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def _execute(sql: str, params: tuple = ()) -> sqlite3.Cursor:
|
|
203
|
+
def _run():
|
|
204
|
+
conn = _get_db()
|
|
205
|
+
return conn.execute(sql, params)
|
|
206
|
+
return await asyncio.to_thread(_run)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def _execute_commit(sql: str, params: tuple = ()) -> sqlite3.Cursor:
|
|
210
|
+
def _run():
|
|
211
|
+
conn = _get_db()
|
|
212
|
+
with _write_lock:
|
|
213
|
+
cur = conn.execute(sql, params)
|
|
214
|
+
conn.commit()
|
|
215
|
+
return cur
|
|
216
|
+
return await asyncio.to_thread(_run)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def _fetch_all(sql: str, params: tuple = ()) -> list[sqlite3.Row]:
|
|
220
|
+
def _run():
|
|
221
|
+
conn = _get_db()
|
|
222
|
+
return conn.execute(sql, params).fetchall()
|
|
223
|
+
return await asyncio.to_thread(_run)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def _fetch_one(sql: str, params: tuple = ()) -> sqlite3.Row | None:
|
|
227
|
+
def _run():
|
|
228
|
+
conn = _get_db()
|
|
229
|
+
return conn.execute(sql, params).fetchone()
|
|
230
|
+
return await asyncio.to_thread(_run)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
async def _write_transaction(callback: Callable[[sqlite3.Connection], T]) -> T:
|
|
234
|
+
def _run() -> T:
|
|
235
|
+
conn = _get_db()
|
|
236
|
+
with _write_lock:
|
|
237
|
+
try:
|
|
238
|
+
result = callback(conn)
|
|
239
|
+
conn.commit()
|
|
240
|
+
return result
|
|
241
|
+
except Exception:
|
|
242
|
+
conn.rollback()
|
|
243
|
+
raise
|
|
244
|
+
|
|
245
|
+
return await asyncio.to_thread(_run)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Persisted UI transcript rows for restoring terminal output trees."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from voidx.memory.store import _fetch_all, _write_transaction
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _now() -> str:
|
|
15
|
+
return datetime.now(timezone.utc).isoformat()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TranscriptTurnRow(BaseModel):
|
|
19
|
+
session_id: str
|
|
20
|
+
turn_id: int
|
|
21
|
+
user_message_id: int | None = None
|
|
22
|
+
created_at: str = Field(default_factory=_now)
|
|
23
|
+
completed_at: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TranscriptNodeRow(BaseModel):
|
|
27
|
+
session_id: str
|
|
28
|
+
turn_id: int
|
|
29
|
+
node_id: int
|
|
30
|
+
parent_node_id: int | None = None
|
|
31
|
+
sort_order: int
|
|
32
|
+
node_type: str
|
|
33
|
+
header: str = ""
|
|
34
|
+
body_lines: list[str] = Field(default_factory=list)
|
|
35
|
+
status: str = "running"
|
|
36
|
+
collapsed: bool = False
|
|
37
|
+
elapsed: float | None = None
|
|
38
|
+
message_id: int | None = None
|
|
39
|
+
tool_call_id: str | None = None
|
|
40
|
+
agent_run_id: str | None = None
|
|
41
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
42
|
+
created_at: str = Field(default_factory=_now)
|
|
43
|
+
updated_at: str = Field(default_factory=_now)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def replace_transcript(
|
|
47
|
+
session_id: str,
|
|
48
|
+
nodes: list[TranscriptNodeRow],
|
|
49
|
+
*,
|
|
50
|
+
turn_count: int | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Replace a session transcript snapshot atomically."""
|
|
53
|
+
now = _now()
|
|
54
|
+
if turn_count is None:
|
|
55
|
+
turn_ids = sorted({node.turn_id for node in nodes})
|
|
56
|
+
else:
|
|
57
|
+
turn_ids = list(range(turn_count))
|
|
58
|
+
|
|
59
|
+
def _run(conn):
|
|
60
|
+
conn.execute("DELETE FROM transcript_nodes WHERE session_id = ?", (session_id,))
|
|
61
|
+
conn.execute("DELETE FROM turns WHERE session_id = ?", (session_id,))
|
|
62
|
+
for turn_id in turn_ids:
|
|
63
|
+
conn.execute(
|
|
64
|
+
"""INSERT INTO turns (session_id, turn_id, created_at, completed_at)
|
|
65
|
+
VALUES (?, ?, ?, ?)""",
|
|
66
|
+
(session_id, turn_id, now, now),
|
|
67
|
+
)
|
|
68
|
+
for node in nodes:
|
|
69
|
+
conn.execute(
|
|
70
|
+
"""INSERT INTO transcript_nodes (
|
|
71
|
+
session_id, turn_id, node_id, parent_node_id, sort_order,
|
|
72
|
+
node_type, header, body_json, status, collapsed, elapsed,
|
|
73
|
+
message_id, tool_call_id, agent_run_id, metadata_json,
|
|
74
|
+
created_at, updated_at
|
|
75
|
+
)
|
|
76
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
77
|
+
(
|
|
78
|
+
session_id,
|
|
79
|
+
node.turn_id,
|
|
80
|
+
node.node_id,
|
|
81
|
+
node.parent_node_id,
|
|
82
|
+
node.sort_order,
|
|
83
|
+
node.node_type,
|
|
84
|
+
node.header,
|
|
85
|
+
json.dumps(node.body_lines, ensure_ascii=False),
|
|
86
|
+
node.status,
|
|
87
|
+
1 if node.collapsed else 0,
|
|
88
|
+
node.elapsed,
|
|
89
|
+
node.message_id,
|
|
90
|
+
node.tool_call_id,
|
|
91
|
+
node.agent_run_id,
|
|
92
|
+
json.dumps(node.metadata, ensure_ascii=False),
|
|
93
|
+
node.created_at,
|
|
94
|
+
node.updated_at,
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
await _write_transaction(_run)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def load_transcript(session_id: str) -> list[TranscriptNodeRow]:
|
|
102
|
+
rows = await _fetch_all(
|
|
103
|
+
"""SELECT * FROM transcript_nodes
|
|
104
|
+
WHERE session_id = ?
|
|
105
|
+
ORDER BY turn_id ASC, sort_order ASC, node_id ASC""",
|
|
106
|
+
(session_id,),
|
|
107
|
+
)
|
|
108
|
+
return [
|
|
109
|
+
TranscriptNodeRow(
|
|
110
|
+
session_id=row["session_id"],
|
|
111
|
+
turn_id=row["turn_id"],
|
|
112
|
+
node_id=row["node_id"],
|
|
113
|
+
parent_node_id=row["parent_node_id"],
|
|
114
|
+
sort_order=row["sort_order"],
|
|
115
|
+
node_type=row["node_type"],
|
|
116
|
+
header=row["header"],
|
|
117
|
+
body_lines=json.loads(row["body_json"] or "[]"),
|
|
118
|
+
status=row["status"],
|
|
119
|
+
collapsed=bool(row["collapsed"]),
|
|
120
|
+
elapsed=row["elapsed"],
|
|
121
|
+
message_id=row["message_id"],
|
|
122
|
+
tool_call_id=row["tool_call_id"],
|
|
123
|
+
agent_run_id=row["agent_run_id"],
|
|
124
|
+
metadata=json.loads(row["metadata_json"] or "{}"),
|
|
125
|
+
created_at=row["created_at"],
|
|
126
|
+
updated_at=row["updated_at"],
|
|
127
|
+
)
|
|
128
|
+
for row in rows
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def clear_transcript(session_id: str) -> None:
|
|
133
|
+
def _run(conn):
|
|
134
|
+
conn.execute("DELETE FROM transcript_nodes WHERE session_id = ?", (session_id,))
|
|
135
|
+
conn.execute("DELETE FROM turns WHERE session_id = ?", (session_id,))
|
|
136
|
+
|
|
137
|
+
await _write_transaction(_run)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Permission system — aligned with opencode/Claude Code architecture."""
|
|
2
|
+
|
|
3
|
+
from voidx.permission.schema import Action, Rule, Ruleset
|
|
4
|
+
from voidx.permission.wildcard import match as wildcard_match
|
|
5
|
+
from voidx.permission.evaluate import evaluate, from_config, merge
|
|
6
|
+
from voidx.permission.service import PermissionService
|
|
7
|
+
from voidx.permission.sandbox import check_sandbox_filepath, check_sandbox_bash
|
|
8
|
+
from voidx.permission.engine import (
|
|
9
|
+
PermissionCapability,
|
|
10
|
+
PermissionContext,
|
|
11
|
+
PermissionDecision,
|
|
12
|
+
authorize_tool_call,
|
|
13
|
+
classify_tool_call,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Action", "Rule", "Ruleset",
|
|
18
|
+
"wildcard_match",
|
|
19
|
+
"evaluate", "from_config", "merge",
|
|
20
|
+
"PermissionService",
|
|
21
|
+
"check_sandbox_filepath",
|
|
22
|
+
"check_sandbox_bash",
|
|
23
|
+
"PermissionCapability",
|
|
24
|
+
"PermissionContext",
|
|
25
|
+
"PermissionDecision",
|
|
26
|
+
"authorize_tool_call",
|
|
27
|
+
"classify_tool_call",
|
|
28
|
+
]
|