netizen-cli 0.10.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.
- netizen_cli/__init__.py +3 -0
- netizen_cli/__main__.py +4 -0
- netizen_cli/admin/__init__.py +1 -0
- netizen_cli/admin/auth.py +928 -0
- netizen_cli/admin/errors.py +9 -0
- netizen_cli/admin/port_config.py +115 -0
- netizen_cli/admin/presentation.py +257 -0
- netizen_cli/admin/queries.py +337 -0
- netizen_cli/admin/static/admin.css +260 -0
- netizen_cli/admin/static/admin.js +2898 -0
- netizen_cli/admin/static/index.html +327 -0
- netizen_cli/admin/transport.py +935 -0
- netizen_cli/admin/web.py +2717 -0
- netizen_cli/bindings.py +3215 -0
- netizen_cli/builtin_skills.py +93 -0
- netizen_cli/cards/__init__.py +105 -0
- netizen_cli/cards/callbacks.py +565 -0
- netizen_cli/cards/controls.py +2273 -0
- netizen_cli/cards/defaults.py +213 -0
- netizen_cli/cards/model_info.py +80 -0
- netizen_cli/cards/questions.py +220 -0
- netizen_cli/cards/reply.py +2247 -0
- netizen_cli/cards/scheduled.py +836 -0
- netizen_cli/channel/__init__.py +1 -0
- netizen_cli/channel/completion_mentions.py +60 -0
- netizen_cli/channel/input_preparation.py +644 -0
- netizen_cli/channel/messages.py +57 -0
- netizen_cli/channel/ports.py +52 -0
- netizen_cli/channel/question_inputs.py +51 -0
- netizen_cli/channel/reactions.py +293 -0
- netizen_cli/channel/reply_presenter.py +1505 -0
- netizen_cli/channel/topics.py +70 -0
- netizen_cli/channel_app.py +6593 -0
- netizen_cli/cli.py +287 -0
- netizen_cli/cli_data.py +536 -0
- netizen_cli/cli_packages.py +526 -0
- netizen_cli/cli_services.py +651 -0
- netizen_cli/cli_setup.py +242 -0
- netizen_cli/cli_update.py +303 -0
- netizen_cli/cli_update_restore.py +53 -0
- netizen_cli/cli_update_worker.py +333 -0
- netizen_cli/codex_runtime.py +7125 -0
- netizen_cli/completion_mention.py +16 -0
- netizen_cli/database_migrations.py +218 -0
- netizen_cli/defaults/__init__.py +5 -0
- netizen_cli/defaults/models.py +39 -0
- netizen_cli/defaults/service.py +232 -0
- netizen_cli/defaults/store.py +260 -0
- netizen_cli/deployment/__init__.py +1 -0
- netizen_cli/deployment/restart_worker.py +134 -0
- netizen_cli/deployment/update_executor.py +258 -0
- netizen_cli/deployment/update_protocol.py +281 -0
- netizen_cli/domain.py +416 -0
- netizen_cli/error_messages.py +124 -0
- netizen_cli/experience.py +531 -0
- netizen_cli/feishu_app_onboarding.py +187 -0
- netizen_cli/feishu_app_permissions.py +123 -0
- netizen_cli/git_status.py +63 -0
- netizen_cli/image_inputs.py +579 -0
- netizen_cli/instance.py +84 -0
- netizen_cli/lark_app.py +125 -0
- netizen_cli/main.py +903 -0
- netizen_cli/management/__init__.py +83 -0
- netizen_cli/management/blocking_io.py +352 -0
- netizen_cli/management/chat_labels.py +266 -0
- netizen_cli/management/coordination.py +32 -0
- netizen_cli/management/service.py +2187 -0
- netizen_cli/management/updates.py +214 -0
- netizen_cli/markdown_images.py +78 -0
- netizen_cli/message_content.py +786 -0
- netizen_cli/message_history.py +643 -0
- netizen_cli/message_preparation.py +60 -0
- netizen_cli/message_projection.py +923 -0
- netizen_cli/migrations/__init__.py +1 -0
- netizen_cli/migrations/schema.py +103 -0
- netizen_cli/migrations/v14.py +438 -0
- netizen_cli/model_settings.py +269 -0
- netizen_cli/package_resources.py +22 -0
- netizen_cli/projects.py +327 -0
- netizen_cli/prompt_projection.py +327 -0
- netizen_cli/quoted_context.py +312 -0
- netizen_cli/resources/config.example.yaml +35 -0
- netizen_cli/resources/skills/netizen-lark/SKILL.md +64 -0
- netizen_cli/resources/skills/netizen-user-guide/SKILL.md +37 -0
- netizen_cli/resources/skills/netizen-user-guide/references/user-guide.md +842 -0
- netizen_cli/result_images.py +123 -0
- netizen_cli/runtime/__init__.py +1 -0
- netizen_cli/runtime/contracts.py +792 -0
- netizen_cli/runtime/name_writes.py +67 -0
- netizen_cli/runtime/thread_naming.py +451 -0
- netizen_cli/schedules/__init__.py +1 -0
- netizen_cli/schedules/mcp.py +535 -0
- netizen_cli/schedules/models.py +394 -0
- netizen_cli/schedules/scheduler.py +374 -0
- netizen_cli/schedules/service.py +766 -0
- netizen_cli/schedules/store.py +771 -0
- netizen_cli/sdk_gap_adapter.py +1151 -0
- netizen_cli/service_launcher.py +583 -0
- netizen_cli/session_settings.py +126 -0
- netizen_cli/settings.py +216 -0
- netizen_cli/skill_references.py +40 -0
- netizen_cli/terminal_cleanup.py +155 -0
- netizen_cli/turn_activity.py +688 -0
- netizen_cli/turn_files.py +812 -0
- netizen_cli/turn_patch_children.py +254 -0
- netizen_cli/turn_plan_observer.py +315 -0
- netizen_cli/user_questions.py +106 -0
- netizen_cli-0.10.0.dist-info/METADATA +18 -0
- netizen_cli-0.10.0.dist-info/RECORD +112 -0
- netizen_cli-0.10.0.dist-info/WHEEL +5 -0
- netizen_cli-0.10.0.dist-info/entry_points.txt +2 -0
- netizen_cli-0.10.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Immutable version-specific Channel database migration contracts."""
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Structural validation shared by frozen migration contracts, using SQLite metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sqlite3
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_TOKEN = re.compile(r"--[^\n]*|/\*.*?\*/|'(?:''|[^'])*'|\"(?:\"\"|[^\"])*\"|`(?:``|[^`])*`|\[[^]]*\]|\w+|[^\s]", re.DOTALL)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _tokens(sql: str) -> tuple[str, ...]:
|
|
13
|
+
# Ignore presentation whitespace and identifier quoting, but preserve string
|
|
14
|
+
# literals: CHECK(value = 'A') and CHECK(value = 'a') are different contracts.
|
|
15
|
+
return tuple(
|
|
16
|
+
token if token.startswith("'") else token.strip('"`[]').lower()
|
|
17
|
+
for token in _TOKEN.findall(sql)
|
|
18
|
+
if not token.startswith(("--", "/*"))
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _checks(sql: str) -> tuple[tuple[str, ...], ...]:
|
|
23
|
+
tokens = _tokens(sql)
|
|
24
|
+
checks = []
|
|
25
|
+
for start, token in enumerate(tokens):
|
|
26
|
+
if token != "check" or tokens[start + 1:start + 2] != ("(",):
|
|
27
|
+
continue
|
|
28
|
+
depth = 0
|
|
29
|
+
for end in range(start + 1, len(tokens)):
|
|
30
|
+
depth += (tokens[end] == "(") - (tokens[end] == ")")
|
|
31
|
+
if depth == 0:
|
|
32
|
+
checks.append(tokens[start + 1:end + 1])
|
|
33
|
+
break
|
|
34
|
+
return tuple(sorted(checks))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _quote(name: str) -> str:
|
|
38
|
+
return '"' + name.replace('"', '""') + '"'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _table_shape(connection: sqlite3.Connection, name: str, sql: str) -> tuple:
|
|
42
|
+
tokens = _tokens(sql)
|
|
43
|
+
if any(tokens[index:index + 2] == ("on", "conflict") for index in range(len(tokens))):
|
|
44
|
+
# PRAGMA metadata omits constraint conflict policies. Frozen schemas use
|
|
45
|
+
# SQLite's implicit ABORT policy; reject explicit policies rather than
|
|
46
|
+
# incorrectly treating REPLACE/IGNORE as equivalent. Supporting a future
|
|
47
|
+
# schema with explicit policies requires a versioned validator extension.
|
|
48
|
+
raise RuntimeError(f"Channel database has unsupported conflict policy: {name}")
|
|
49
|
+
quoted = _quote(name)
|
|
50
|
+
columns = sorted(tuple(row)[1:] for row in connection.execute(f"PRAGMA table_xinfo({quoted})"))
|
|
51
|
+
foreign_keys = sorted(tuple(row)[1:] for row in connection.execute(f"PRAGMA foreign_key_list({quoted})"))
|
|
52
|
+
unique_indexes = []
|
|
53
|
+
for index in connection.execute(f"PRAGMA index_list({quoted})"):
|
|
54
|
+
if not index[2]:
|
|
55
|
+
continue # Compatible query indexes are not schema-version changes.
|
|
56
|
+
index_name = index[1]
|
|
57
|
+
columns_and_order = tuple(
|
|
58
|
+
# Column position can differ after a supported table rebuild.
|
|
59
|
+
(row[0], row[2], row[3], row[4], row[5])
|
|
60
|
+
for row in connection.execute(f"PRAGMA index_xinfo({_quote(index_name)})")
|
|
61
|
+
)
|
|
62
|
+
index_sql = connection.execute(
|
|
63
|
+
"SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?", (index_name,),
|
|
64
|
+
).fetchone()[0]
|
|
65
|
+
unique_indexes.append((
|
|
66
|
+
index[3], index[4], columns_and_order,
|
|
67
|
+
_tokens(index_sql or "") if index[3] == "c" else (),
|
|
68
|
+
))
|
|
69
|
+
return tuple(columns), tuple(foreign_keys), tuple(sorted(unique_indexes, key=repr)), _checks(sql)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def require_schema(connection: sqlite3.Connection, statements: tuple[str, ...]) -> None:
|
|
73
|
+
"""Match a frozen schema without depending on CREATE text or column order.
|
|
74
|
+
|
|
75
|
+
Unique constraints, checks and triggers are part of the version's contract;
|
|
76
|
+
non-unique performance indexes may evolve without a schema migration.
|
|
77
|
+
Explicit ON CONFLICT policies are outside this validator's supported shape.
|
|
78
|
+
"""
|
|
79
|
+
reference = sqlite3.connect(":memory:")
|
|
80
|
+
try:
|
|
81
|
+
for statement in statements:
|
|
82
|
+
reference.execute(statement)
|
|
83
|
+
objects = {}
|
|
84
|
+
for source in (reference, connection):
|
|
85
|
+
objects[source] = {
|
|
86
|
+
(row[0], row[1]): row[2]
|
|
87
|
+
for row in source.execute(
|
|
88
|
+
"SELECT type, name, sql FROM sqlite_master "
|
|
89
|
+
"WHERE type IN ('table', 'view', 'trigger') AND name NOT LIKE 'sqlite_%'"
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
expected, actual = objects[reference], objects[connection]
|
|
93
|
+
if expected.keys() != actual.keys():
|
|
94
|
+
raise RuntimeError("Channel database schema objects do not match the declared version")
|
|
95
|
+
for (kind, name), sql in expected.items():
|
|
96
|
+
if kind == "table":
|
|
97
|
+
matches = _table_shape(connection, name, actual[kind, name]) == _table_shape(reference, name, sql)
|
|
98
|
+
else:
|
|
99
|
+
matches = _tokens(actual[kind, name]) == _tokens(sql)
|
|
100
|
+
if not matches:
|
|
101
|
+
raise RuntimeError(f"Channel database has invalid {kind} structure: {name}")
|
|
102
|
+
finally:
|
|
103
|
+
reference.close()
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""Frozen v14 baseline; never derive historical schemas from current runtime code.
|
|
2
|
+
|
|
3
|
+
This is validation evidence, not an installation or upgrade script. New databases
|
|
4
|
+
continue to be created by BindingStore. Keep this baseline unchanged when the
|
|
5
|
+
runtime schema advances.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
|
|
13
|
+
from .schema import require_schema
|
|
14
|
+
|
|
15
|
+
SCHEMA = (
|
|
16
|
+
"""CREATE TABLE bindings (
|
|
17
|
+
binding_id TEXT PRIMARY KEY,
|
|
18
|
+
scope_key TEXT NOT NULL REFERENCES scopes(scope_key),
|
|
19
|
+
project_alias TEXT NOT NULL,
|
|
20
|
+
native_thread_id TEXT UNIQUE,
|
|
21
|
+
model_id TEXT,
|
|
22
|
+
effort_id TEXT,
|
|
23
|
+
service_tier_id TEXT,
|
|
24
|
+
settings_revision INTEGER NOT NULL DEFAULT 1
|
|
25
|
+
CHECK(settings_revision >= 1),
|
|
26
|
+
message_context_mode TEXT NOT NULL DEFAULT 'current-only'
|
|
27
|
+
CHECK(
|
|
28
|
+
message_context_mode IN ('current-only', 'catch-up')
|
|
29
|
+
),
|
|
30
|
+
context_anchor_message_id TEXT,
|
|
31
|
+
context_anchor_create_time_ms INTEGER,
|
|
32
|
+
context_revision INTEGER NOT NULL DEFAULT 1
|
|
33
|
+
CHECK(
|
|
34
|
+
typeof(context_revision) = 'integer'
|
|
35
|
+
AND context_revision >= 1
|
|
36
|
+
),
|
|
37
|
+
task_reactions_enabled INTEGER NOT NULL DEFAULT 0
|
|
38
|
+
CHECK(
|
|
39
|
+
typeof(task_reactions_enabled) = 'integer'
|
|
40
|
+
AND task_reactions_enabled IN (0, 1)
|
|
41
|
+
),
|
|
42
|
+
progress_card_enabled INTEGER NOT NULL DEFAULT 0
|
|
43
|
+
CHECK(
|
|
44
|
+
typeof(progress_card_enabled) = 'integer'
|
|
45
|
+
AND progress_card_enabled IN (0, 1)
|
|
46
|
+
),
|
|
47
|
+
completion_mention_enabled INTEGER NOT NULL DEFAULT 1
|
|
48
|
+
CHECK(
|
|
49
|
+
typeof(completion_mention_enabled) = 'integer'
|
|
50
|
+
AND completion_mention_enabled IN (0, 1)
|
|
51
|
+
),
|
|
52
|
+
feedback_revision INTEGER NOT NULL DEFAULT 1
|
|
53
|
+
CHECK(
|
|
54
|
+
typeof(feedback_revision) = 'integer'
|
|
55
|
+
AND feedback_revision >= 1
|
|
56
|
+
),
|
|
57
|
+
creator_id TEXT NOT NULL,
|
|
58
|
+
created_at TEXT NOT NULL,
|
|
59
|
+
activated_at TEXT NOT NULL,
|
|
60
|
+
ever_activated INTEGER NOT NULL DEFAULT 1
|
|
61
|
+
CHECK(ever_activated IN (0, 1)),
|
|
62
|
+
CHECK(
|
|
63
|
+
(
|
|
64
|
+
model_id IS NULL
|
|
65
|
+
AND effort_id IS NULL
|
|
66
|
+
AND service_tier_id IS NULL
|
|
67
|
+
) OR (
|
|
68
|
+
model_id IS NOT NULL
|
|
69
|
+
AND effort_id IS NOT NULL
|
|
70
|
+
AND service_tier_id IS NOT NULL
|
|
71
|
+
)
|
|
72
|
+
),
|
|
73
|
+
CHECK(
|
|
74
|
+
(
|
|
75
|
+
message_context_mode = 'current-only'
|
|
76
|
+
AND context_anchor_message_id IS NULL
|
|
77
|
+
AND context_anchor_create_time_ms IS NULL
|
|
78
|
+
) OR (
|
|
79
|
+
message_context_mode = 'catch-up'
|
|
80
|
+
AND context_anchor_message_id IS NOT NULL
|
|
81
|
+
AND length(context_anchor_message_id) > 0
|
|
82
|
+
AND context_anchor_create_time_ms IS NOT NULL
|
|
83
|
+
AND typeof(context_anchor_create_time_ms) = 'integer'
|
|
84
|
+
AND context_anchor_create_time_ms > 0
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
)""",
|
|
88
|
+
"""CREATE TABLE dedup_keys (
|
|
89
|
+
dedup_key TEXT PRIMARY KEY,
|
|
90
|
+
expires_at REAL NOT NULL
|
|
91
|
+
)""",
|
|
92
|
+
"""CREATE TABLE projects (
|
|
93
|
+
alias TEXT PRIMARY KEY,
|
|
94
|
+
cwd TEXT NOT NULL,
|
|
95
|
+
enabled INTEGER NOT NULL CHECK(enabled IN (0, 1)),
|
|
96
|
+
deleted INTEGER NOT NULL DEFAULT 0
|
|
97
|
+
CHECK(typeof(deleted) = 'integer' AND deleted IN (0, 1)),
|
|
98
|
+
revision INTEGER NOT NULL CHECK(revision >= 1),
|
|
99
|
+
created_at TEXT NOT NULL,
|
|
100
|
+
updated_at TEXT NOT NULL
|
|
101
|
+
)""",
|
|
102
|
+
"""CREATE TABLE schedule_plans (
|
|
103
|
+
plan_id TEXT PRIMARY KEY, revision INTEGER NOT NULL CHECK(revision >= 1),
|
|
104
|
+
name TEXT NOT NULL, instructions TEXT NOT NULL, project_alias TEXT NOT NULL,
|
|
105
|
+
app_id TEXT NOT NULL, chat_id TEXT NOT NULL, schedule_json TEXT,
|
|
106
|
+
enabled INTEGER NOT NULL CHECK(enabled IN (0,1)), next_due_at REAL,
|
|
107
|
+
processed_through REAL, created_at REAL NOT NULL, updated_at REAL NOT NULL,
|
|
108
|
+
source TEXT NOT NULL, deleted INTEGER NOT NULL DEFAULT 0 CHECK(deleted IN (0,1)),
|
|
109
|
+
session_settings_json TEXT,
|
|
110
|
+
target_kind TEXT NOT NULL DEFAULT 'new_topic' CHECK(target_kind IN ('new_topic','binding')),
|
|
111
|
+
target_binding_id TEXT,
|
|
112
|
+
CHECK((target_kind = 'new_topic' AND target_binding_id IS NULL) OR
|
|
113
|
+
(target_kind = 'binding' AND target_binding_id IS NOT NULL AND session_settings_json IS NULL)),
|
|
114
|
+
CHECK(deleted = 0 OR (enabled = 0 AND instructions = '' AND schedule_json IS NULL))
|
|
115
|
+
)""",
|
|
116
|
+
"""CREATE TABLE schedule_requests (
|
|
117
|
+
request_id TEXT PRIMARY KEY, operation TEXT NOT NULL, payload_digest TEXT NOT NULL,
|
|
118
|
+
plan_id TEXT NOT NULL, revision INTEGER NOT NULL, expires_at REAL NOT NULL,
|
|
119
|
+
run_id TEXT
|
|
120
|
+
)""",
|
|
121
|
+
"""CREATE TABLE schedule_runs (
|
|
122
|
+
run_id TEXT PRIMARY KEY, plan_id TEXT NOT NULL REFERENCES schedule_plans(plan_id),
|
|
123
|
+
plan_revision INTEGER NOT NULL, due_at REAL NOT NULL,
|
|
124
|
+
project_alias TEXT NOT NULL, app_id TEXT NOT NULL, chat_id TEXT NOT NULL,
|
|
125
|
+
phase TEXT NOT NULL CHECK(phase IN ('claimed','publishing_topic','binding_ready','starting_turn','handed_off','released')),
|
|
126
|
+
barrier TEXT NOT NULL CHECK(barrier IN ('held','unknown','released')),
|
|
127
|
+
root_uuid TEXT NOT NULL UNIQUE, seed_uuid TEXT NOT NULL UNIQUE,
|
|
128
|
+
root_message_id TEXT, topic_id TEXT, origin_message_id TEXT,
|
|
129
|
+
binding_id TEXT, initial_turn_id TEXT, error_code TEXT, delivery_state TEXT,
|
|
130
|
+
created_at REAL NOT NULL, updated_at REAL NOT NULL,
|
|
131
|
+
missed_from REAL, missed_count INTEGER NOT NULL DEFAULT 0,
|
|
132
|
+
binding_removed INTEGER NOT NULL DEFAULT 0 CHECK(binding_removed IN (0,1)),
|
|
133
|
+
trigger_source TEXT NOT NULL DEFAULT 'scheduled' CHECK(trigger_source IN ('scheduled','manual')),
|
|
134
|
+
target_kind TEXT NOT NULL DEFAULT 'new_topic' CHECK(target_kind IN ('new_topic','binding')),
|
|
135
|
+
disposition TEXT CHECK(disposition IN ('started','steered')),
|
|
136
|
+
CHECK(target_kind = 'new_topic' OR binding_id IS NOT NULL),
|
|
137
|
+
CHECK(disposition IS NULL OR (target_kind = 'binding' AND initial_turn_id IS NOT NULL AND barrier = 'released'))
|
|
138
|
+
)""",
|
|
139
|
+
"""CREATE TABLE schema_version (version INTEGER NOT NULL)""",
|
|
140
|
+
"""CREATE TABLE scopes (
|
|
141
|
+
scope_key TEXT PRIMARY KEY,
|
|
142
|
+
app_id TEXT NOT NULL,
|
|
143
|
+
chat_id TEXT NOT NULL,
|
|
144
|
+
kind TEXT NOT NULL,
|
|
145
|
+
topic_id TEXT,
|
|
146
|
+
active_binding_id TEXT,
|
|
147
|
+
updated_at TEXT NOT NULL
|
|
148
|
+
)""",
|
|
149
|
+
"""CREATE TABLE session_defaults (
|
|
150
|
+
id TEXT PRIMARY KEY,
|
|
151
|
+
app_id TEXT NOT NULL CHECK(length(app_id) > 0),
|
|
152
|
+
kind TEXT NOT NULL CHECK(kind IN ('chat', 'group_name')),
|
|
153
|
+
chat_id TEXT, keyword TEXT,
|
|
154
|
+
project TEXT NOT NULL CHECK(length(project) > 0),
|
|
155
|
+
session_settings_json TEXT NOT NULL,
|
|
156
|
+
revision INTEGER NOT NULL CHECK(typeof(revision) = 'integer' AND revision >= 1),
|
|
157
|
+
position INTEGER,
|
|
158
|
+
CHECK((kind = 'chat' AND chat_id IS NOT NULL AND length(chat_id) > 0
|
|
159
|
+
AND keyword IS NULL AND position IS NULL) OR
|
|
160
|
+
(kind = 'group_name' AND chat_id IS NULL AND keyword IS NOT NULL
|
|
161
|
+
AND length(trim(keyword)) > 0 AND typeof(position) = 'integer' AND position >= 0))
|
|
162
|
+
)""",
|
|
163
|
+
"""CREATE TABLE session_defaults_order (
|
|
164
|
+
app_id TEXT PRIMARY KEY CHECK(length(app_id) > 0),
|
|
165
|
+
revision INTEGER NOT NULL CHECK(typeof(revision) = 'integer' AND revision >= 1)
|
|
166
|
+
)""",
|
|
167
|
+
"""CREATE TABLE side_topics (
|
|
168
|
+
side_id TEXT PRIMARY KEY,
|
|
169
|
+
app_id TEXT NOT NULL,
|
|
170
|
+
chat_id TEXT NOT NULL,
|
|
171
|
+
topic_id TEXT,
|
|
172
|
+
root_message_id TEXT,
|
|
173
|
+
source_message_id TEXT NOT NULL,
|
|
174
|
+
parent_binding_id TEXT NOT NULL,
|
|
175
|
+
creator_id TEXT NOT NULL,
|
|
176
|
+
requires_mention INTEGER NOT NULL
|
|
177
|
+
CHECK(requires_mention IN (0, 1)),
|
|
178
|
+
state TEXT NOT NULL
|
|
179
|
+
CHECK(state IN ('creating', 'open', 'closed', 'expired', 'failed')),
|
|
180
|
+
created_at TEXT NOT NULL,
|
|
181
|
+
updated_at TEXT NOT NULL,
|
|
182
|
+
UNIQUE(app_id, source_message_id)
|
|
183
|
+
)""",
|
|
184
|
+
"""CREATE INDEX bindings_by_scope
|
|
185
|
+
ON bindings(scope_key, activated_at DESC, created_at DESC)""",
|
|
186
|
+
"""CREATE INDEX bindings_global_created
|
|
187
|
+
ON bindings(created_at DESC, binding_id DESC)
|
|
188
|
+
""",
|
|
189
|
+
"""CREATE INDEX bindings_project_created
|
|
190
|
+
ON bindings(project_alias, created_at DESC, binding_id DESC)
|
|
191
|
+
""",
|
|
192
|
+
"""CREATE INDEX bindings_scope_created
|
|
193
|
+
ON bindings(scope_key, created_at DESC, binding_id DESC)
|
|
194
|
+
""",
|
|
195
|
+
"""CREATE INDEX schedule_plans_due ON schedule_plans(app_id, enabled, next_due_at) WHERE deleted = 0""",
|
|
196
|
+
"""CREATE INDEX schedule_plans_project ON schedule_plans(project_alias, deleted)""",
|
|
197
|
+
"""CREATE UNIQUE INDEX schedule_runs_barrier ON schedule_runs(plan_id) WHERE barrier != 'released'""",
|
|
198
|
+
"""CREATE UNIQUE INDEX schedule_runs_binding ON schedule_runs(binding_id) WHERE binding_id IS NOT NULL AND target_kind = 'new_topic'""",
|
|
199
|
+
"""CREATE UNIQUE INDEX schedule_runs_due ON schedule_runs(plan_id, due_at) WHERE trigger_source = 'scheduled'""",
|
|
200
|
+
"""CREATE INDEX schedule_runs_plan ON schedule_runs(plan_id, due_at DESC, run_id DESC)""",
|
|
201
|
+
"""CREATE UNIQUE INDEX schedule_runs_root ON schedule_runs(app_id, chat_id, root_message_id) WHERE root_message_id IS NOT NULL AND target_kind = 'new_topic'""",
|
|
202
|
+
"""CREATE UNIQUE INDEX schedule_runs_topic ON schedule_runs(app_id, chat_id, topic_id) WHERE topic_id IS NOT NULL AND target_kind = 'new_topic'""",
|
|
203
|
+
"""CREATE INDEX scopes_chat_topic_kind
|
|
204
|
+
ON scopes(chat_id, topic_id, kind, scope_key)
|
|
205
|
+
""",
|
|
206
|
+
"""CREATE INDEX scopes_kind_chat_topic
|
|
207
|
+
ON scopes(kind, chat_id, topic_id, scope_key)
|
|
208
|
+
""",
|
|
209
|
+
"""CREATE UNIQUE INDEX session_defaults_chat ON session_defaults(app_id, chat_id) WHERE kind = 'chat'""",
|
|
210
|
+
"""CREATE UNIQUE INDEX session_defaults_position ON session_defaults(app_id, position) WHERE kind = 'group_name'""",
|
|
211
|
+
"""CREATE UNIQUE INDEX side_topics_by_root
|
|
212
|
+
ON side_topics(app_id, chat_id, root_message_id)
|
|
213
|
+
WHERE root_message_id IS NOT NULL""",
|
|
214
|
+
"""CREATE UNIQUE INDEX side_topics_by_topic
|
|
215
|
+
ON side_topics(app_id, chat_id, topic_id)
|
|
216
|
+
WHERE topic_id IS NOT NULL""",
|
|
217
|
+
"""CREATE INDEX side_topics_chat_topic_created
|
|
218
|
+
ON side_topics(chat_id, topic_id, created_at DESC, side_id DESC)
|
|
219
|
+
""",
|
|
220
|
+
"""CREATE INDEX side_topics_global_created
|
|
221
|
+
ON side_topics(created_at DESC, side_id DESC)
|
|
222
|
+
""",
|
|
223
|
+
"""CREATE INDEX side_topics_parent_created
|
|
224
|
+
ON side_topics(parent_binding_id, created_at DESC, side_id DESC)
|
|
225
|
+
""",
|
|
226
|
+
"""CREATE INDEX side_topics_state_created
|
|
227
|
+
ON side_topics(state, created_at DESC, side_id DESC)
|
|
228
|
+
""",
|
|
229
|
+
"""CREATE TRIGGER bindings_context_scope_insert
|
|
230
|
+
BEFORE INSERT ON bindings
|
|
231
|
+
WHEN NEW.message_context_mode = 'catch-up'
|
|
232
|
+
AND EXISTS (
|
|
233
|
+
SELECT 1
|
|
234
|
+
FROM scopes
|
|
235
|
+
WHERE scope_key = NEW.scope_key
|
|
236
|
+
AND kind = 'direct'
|
|
237
|
+
)
|
|
238
|
+
BEGIN
|
|
239
|
+
SELECT RAISE(
|
|
240
|
+
ABORT,
|
|
241
|
+
'direct Binding cannot use catch-up context'
|
|
242
|
+
);
|
|
243
|
+
END""",
|
|
244
|
+
"""CREATE TRIGGER bindings_context_scope_update
|
|
245
|
+
BEFORE UPDATE OF scope_key, message_context_mode,
|
|
246
|
+
context_anchor_message_id, context_anchor_create_time_ms
|
|
247
|
+
ON bindings
|
|
248
|
+
WHEN NEW.message_context_mode = 'catch-up'
|
|
249
|
+
AND EXISTS (
|
|
250
|
+
SELECT 1
|
|
251
|
+
FROM scopes
|
|
252
|
+
WHERE scope_key = NEW.scope_key
|
|
253
|
+
AND kind = 'direct'
|
|
254
|
+
)
|
|
255
|
+
BEGIN
|
|
256
|
+
SELECT RAISE(
|
|
257
|
+
ABORT,
|
|
258
|
+
'direct Binding cannot use catch-up context'
|
|
259
|
+
);
|
|
260
|
+
END""",
|
|
261
|
+
"""CREATE TRIGGER bindings_context_shape_insert
|
|
262
|
+
BEFORE INSERT ON bindings
|
|
263
|
+
WHEN NOT (
|
|
264
|
+
(
|
|
265
|
+
NEW.message_context_mode = 'current-only'
|
|
266
|
+
AND NEW.context_anchor_message_id IS NULL
|
|
267
|
+
AND NEW.context_anchor_create_time_ms IS NULL
|
|
268
|
+
) OR (
|
|
269
|
+
NEW.message_context_mode = 'catch-up'
|
|
270
|
+
AND NEW.context_anchor_message_id IS NOT NULL
|
|
271
|
+
AND length(NEW.context_anchor_message_id) > 0
|
|
272
|
+
AND NEW.context_anchor_create_time_ms IS NOT NULL
|
|
273
|
+
AND typeof(NEW.context_anchor_create_time_ms) = 'integer'
|
|
274
|
+
AND NEW.context_anchor_create_time_ms > 0
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
BEGIN
|
|
278
|
+
SELECT RAISE(
|
|
279
|
+
ABORT,
|
|
280
|
+
'Binding context must match its mode'
|
|
281
|
+
);
|
|
282
|
+
END""",
|
|
283
|
+
"""CREATE TRIGGER bindings_context_shape_update
|
|
284
|
+
BEFORE UPDATE OF message_context_mode,
|
|
285
|
+
context_anchor_message_id, context_anchor_create_time_ms
|
|
286
|
+
ON bindings
|
|
287
|
+
WHEN NOT (
|
|
288
|
+
(
|
|
289
|
+
NEW.message_context_mode = 'current-only'
|
|
290
|
+
AND NEW.context_anchor_message_id IS NULL
|
|
291
|
+
AND NEW.context_anchor_create_time_ms IS NULL
|
|
292
|
+
) OR (
|
|
293
|
+
NEW.message_context_mode = 'catch-up'
|
|
294
|
+
AND NEW.context_anchor_message_id IS NOT NULL
|
|
295
|
+
AND length(NEW.context_anchor_message_id) > 0
|
|
296
|
+
AND NEW.context_anchor_create_time_ms IS NOT NULL
|
|
297
|
+
AND typeof(NEW.context_anchor_create_time_ms) = 'integer'
|
|
298
|
+
AND NEW.context_anchor_create_time_ms > 0
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
BEGIN
|
|
302
|
+
SELECT RAISE(
|
|
303
|
+
ABORT,
|
|
304
|
+
'Binding context must match its mode'
|
|
305
|
+
);
|
|
306
|
+
END""",
|
|
307
|
+
"""CREATE TRIGGER bindings_turn_settings_insert
|
|
308
|
+
BEFORE INSERT ON bindings
|
|
309
|
+
WHEN NOT (
|
|
310
|
+
(
|
|
311
|
+
NEW.model_id IS NULL
|
|
312
|
+
AND NEW.effort_id IS NULL
|
|
313
|
+
AND NEW.service_tier_id IS NULL
|
|
314
|
+
) OR (
|
|
315
|
+
NEW.model_id IS NOT NULL
|
|
316
|
+
AND NEW.effort_id IS NOT NULL
|
|
317
|
+
AND NEW.service_tier_id IS NOT NULL
|
|
318
|
+
AND length(NEW.model_id) > 0
|
|
319
|
+
AND length(NEW.effort_id) > 0
|
|
320
|
+
AND length(NEW.service_tier_id) > 0
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
BEGIN
|
|
324
|
+
SELECT RAISE(
|
|
325
|
+
ABORT,
|
|
326
|
+
'Binding Turn settings must be all NULL or all set'
|
|
327
|
+
);
|
|
328
|
+
END""",
|
|
329
|
+
"""CREATE TRIGGER bindings_turn_settings_update
|
|
330
|
+
BEFORE UPDATE OF model_id, effort_id, service_tier_id ON bindings
|
|
331
|
+
WHEN NOT (
|
|
332
|
+
(
|
|
333
|
+
NEW.model_id IS NULL
|
|
334
|
+
AND NEW.effort_id IS NULL
|
|
335
|
+
AND NEW.service_tier_id IS NULL
|
|
336
|
+
) OR (
|
|
337
|
+
NEW.model_id IS NOT NULL
|
|
338
|
+
AND NEW.effort_id IS NOT NULL
|
|
339
|
+
AND NEW.service_tier_id IS NOT NULL
|
|
340
|
+
AND length(NEW.model_id) > 0
|
|
341
|
+
AND length(NEW.effort_id) > 0
|
|
342
|
+
AND length(NEW.service_tier_id) > 0
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
BEGIN
|
|
346
|
+
SELECT RAISE(
|
|
347
|
+
ABORT,
|
|
348
|
+
'Binding Turn settings must be all NULL or all set'
|
|
349
|
+
);
|
|
350
|
+
END""",
|
|
351
|
+
"""CREATE TRIGGER scopes_activate_binding
|
|
352
|
+
AFTER UPDATE OF active_binding_id ON scopes
|
|
353
|
+
WHEN NEW.active_binding_id IS NOT NULL
|
|
354
|
+
BEGIN
|
|
355
|
+
UPDATE bindings
|
|
356
|
+
SET ever_activated = 1
|
|
357
|
+
WHERE binding_id = NEW.active_binding_id
|
|
358
|
+
AND scope_key = NEW.scope_key;
|
|
359
|
+
END""",
|
|
360
|
+
"""CREATE TRIGGER scopes_context_kind_update
|
|
361
|
+
BEFORE UPDATE OF kind ON scopes
|
|
362
|
+
WHEN NEW.kind = 'direct'
|
|
363
|
+
AND EXISTS (
|
|
364
|
+
SELECT 1
|
|
365
|
+
FROM bindings
|
|
366
|
+
WHERE scope_key = NEW.scope_key
|
|
367
|
+
AND message_context_mode = 'catch-up'
|
|
368
|
+
)
|
|
369
|
+
BEGIN
|
|
370
|
+
SELECT RAISE(
|
|
371
|
+
ABORT,
|
|
372
|
+
'direct Scope cannot contain catch-up Binding'
|
|
373
|
+
);
|
|
374
|
+
END""",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _require_session_settings(raw: str) -> None:
|
|
379
|
+
# Frozen persistence format. Runtime SessionSettings may evolve after v14.
|
|
380
|
+
value = json.loads(raw)
|
|
381
|
+
fields = {
|
|
382
|
+
"turn_settings", "reaction_pulse_enabled", "progress_card_enabled",
|
|
383
|
+
"completion_mention_enabled", "message_context_mode",
|
|
384
|
+
}
|
|
385
|
+
if not isinstance(value, dict) or value.keys() != fields:
|
|
386
|
+
raise ValueError("invalid session settings fields")
|
|
387
|
+
turn = value["turn_settings"]
|
|
388
|
+
if turn is not None and (
|
|
389
|
+
not isinstance(turn, dict)
|
|
390
|
+
or turn.keys() != {"model_id", "effort_id", "service_tier_id"}
|
|
391
|
+
or not all(isinstance(item, str) and item for item in turn.values())
|
|
392
|
+
):
|
|
393
|
+
raise ValueError("invalid Turn settings")
|
|
394
|
+
if any(type(value[key]) is not bool for key in fields - {"turn_settings", "message_context_mode"}):
|
|
395
|
+
raise ValueError("invalid task feedback")
|
|
396
|
+
if value["message_context_mode"] not in ("current-only", "catch-up"):
|
|
397
|
+
raise ValueError("invalid context mode")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def validate(connection: sqlite3.Connection) -> None:
|
|
401
|
+
"""Validate v14 structure and persisted business values without repairing data."""
|
|
402
|
+
require_schema(connection, SCHEMA)
|
|
403
|
+
if connection.execute(
|
|
404
|
+
"SELECT 1 FROM schedule_runs WHERE (target_kind='binding' AND (binding_id IS NULL OR binding_id='')) "
|
|
405
|
+
"OR (disposition IS NOT NULL AND (target_kind!='binding' "
|
|
406
|
+
"OR initial_turn_id IS NULL OR initial_turn_id='' OR barrier!='released')) LIMIT 1"
|
|
407
|
+
).fetchone():
|
|
408
|
+
raise RuntimeError("schema v14 scheduling input receipt is invalid")
|
|
409
|
+
if connection.execute(
|
|
410
|
+
"SELECT 1 FROM session_defaults_order WHERE app_id IS NULL OR app_id = '' "
|
|
411
|
+
"OR typeof(revision) != 'integer' OR revision < 1 LIMIT 1"
|
|
412
|
+
).fetchone():
|
|
413
|
+
raise RuntimeError("schema v14 defaults order revision is invalid")
|
|
414
|
+
try:
|
|
415
|
+
for row in connection.execute("SELECT deleted, session_settings_json, target_kind, target_binding_id FROM schedule_plans"):
|
|
416
|
+
if (row["target_kind"] == "binding") != bool(row["target_binding_id"]):
|
|
417
|
+
raise ValueError("invalid scheduling target identity")
|
|
418
|
+
if row["target_kind"] == "binding" or row["deleted"]:
|
|
419
|
+
if row["session_settings_json"] is not None:
|
|
420
|
+
raise ValueError("unexpected scheduling session settings")
|
|
421
|
+
else:
|
|
422
|
+
_require_session_settings(row["session_settings_json"])
|
|
423
|
+
positions: dict[str, list[int]] = {}
|
|
424
|
+
for row in connection.execute("SELECT * FROM session_defaults ORDER BY app_id, position"):
|
|
425
|
+
if not all(isinstance(row[key], str) and row[key] for key in ("id", "app_id", "project")):
|
|
426
|
+
raise ValueError("missing default identity")
|
|
427
|
+
_require_session_settings(row["session_settings_json"])
|
|
428
|
+
if row["kind"] == "group_name":
|
|
429
|
+
if row["keyword"] != row["keyword"].strip():
|
|
430
|
+
raise ValueError("invalid group keyword")
|
|
431
|
+
positions.setdefault(row["app_id"], []).append(row["position"])
|
|
432
|
+
for app_id, values in positions.items():
|
|
433
|
+
if len(values) > 200 or values != list(range(len(values))):
|
|
434
|
+
raise ValueError("invalid defaults group order")
|
|
435
|
+
if connection.execute("SELECT 1 FROM session_defaults_order WHERE app_id = ?", (app_id,)).fetchone() is None:
|
|
436
|
+
raise ValueError("missing defaults group order revision")
|
|
437
|
+
except (TypeError, ValueError) as error:
|
|
438
|
+
raise RuntimeError(f"schema v14 persisted configuration is invalid: {error}") from error
|