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.
Files changed (112) hide show
  1. netizen_cli/__init__.py +3 -0
  2. netizen_cli/__main__.py +4 -0
  3. netizen_cli/admin/__init__.py +1 -0
  4. netizen_cli/admin/auth.py +928 -0
  5. netizen_cli/admin/errors.py +9 -0
  6. netizen_cli/admin/port_config.py +115 -0
  7. netizen_cli/admin/presentation.py +257 -0
  8. netizen_cli/admin/queries.py +337 -0
  9. netizen_cli/admin/static/admin.css +260 -0
  10. netizen_cli/admin/static/admin.js +2898 -0
  11. netizen_cli/admin/static/index.html +327 -0
  12. netizen_cli/admin/transport.py +935 -0
  13. netizen_cli/admin/web.py +2717 -0
  14. netizen_cli/bindings.py +3215 -0
  15. netizen_cli/builtin_skills.py +93 -0
  16. netizen_cli/cards/__init__.py +105 -0
  17. netizen_cli/cards/callbacks.py +565 -0
  18. netizen_cli/cards/controls.py +2273 -0
  19. netizen_cli/cards/defaults.py +213 -0
  20. netizen_cli/cards/model_info.py +80 -0
  21. netizen_cli/cards/questions.py +220 -0
  22. netizen_cli/cards/reply.py +2247 -0
  23. netizen_cli/cards/scheduled.py +836 -0
  24. netizen_cli/channel/__init__.py +1 -0
  25. netizen_cli/channel/completion_mentions.py +60 -0
  26. netizen_cli/channel/input_preparation.py +644 -0
  27. netizen_cli/channel/messages.py +57 -0
  28. netizen_cli/channel/ports.py +52 -0
  29. netizen_cli/channel/question_inputs.py +51 -0
  30. netizen_cli/channel/reactions.py +293 -0
  31. netizen_cli/channel/reply_presenter.py +1505 -0
  32. netizen_cli/channel/topics.py +70 -0
  33. netizen_cli/channel_app.py +6593 -0
  34. netizen_cli/cli.py +287 -0
  35. netizen_cli/cli_data.py +536 -0
  36. netizen_cli/cli_packages.py +526 -0
  37. netizen_cli/cli_services.py +651 -0
  38. netizen_cli/cli_setup.py +242 -0
  39. netizen_cli/cli_update.py +303 -0
  40. netizen_cli/cli_update_restore.py +53 -0
  41. netizen_cli/cli_update_worker.py +333 -0
  42. netizen_cli/codex_runtime.py +7125 -0
  43. netizen_cli/completion_mention.py +16 -0
  44. netizen_cli/database_migrations.py +218 -0
  45. netizen_cli/defaults/__init__.py +5 -0
  46. netizen_cli/defaults/models.py +39 -0
  47. netizen_cli/defaults/service.py +232 -0
  48. netizen_cli/defaults/store.py +260 -0
  49. netizen_cli/deployment/__init__.py +1 -0
  50. netizen_cli/deployment/restart_worker.py +134 -0
  51. netizen_cli/deployment/update_executor.py +258 -0
  52. netizen_cli/deployment/update_protocol.py +281 -0
  53. netizen_cli/domain.py +416 -0
  54. netizen_cli/error_messages.py +124 -0
  55. netizen_cli/experience.py +531 -0
  56. netizen_cli/feishu_app_onboarding.py +187 -0
  57. netizen_cli/feishu_app_permissions.py +123 -0
  58. netizen_cli/git_status.py +63 -0
  59. netizen_cli/image_inputs.py +579 -0
  60. netizen_cli/instance.py +84 -0
  61. netizen_cli/lark_app.py +125 -0
  62. netizen_cli/main.py +903 -0
  63. netizen_cli/management/__init__.py +83 -0
  64. netizen_cli/management/blocking_io.py +352 -0
  65. netizen_cli/management/chat_labels.py +266 -0
  66. netizen_cli/management/coordination.py +32 -0
  67. netizen_cli/management/service.py +2187 -0
  68. netizen_cli/management/updates.py +214 -0
  69. netizen_cli/markdown_images.py +78 -0
  70. netizen_cli/message_content.py +786 -0
  71. netizen_cli/message_history.py +643 -0
  72. netizen_cli/message_preparation.py +60 -0
  73. netizen_cli/message_projection.py +923 -0
  74. netizen_cli/migrations/__init__.py +1 -0
  75. netizen_cli/migrations/schema.py +103 -0
  76. netizen_cli/migrations/v14.py +438 -0
  77. netizen_cli/model_settings.py +269 -0
  78. netizen_cli/package_resources.py +22 -0
  79. netizen_cli/projects.py +327 -0
  80. netizen_cli/prompt_projection.py +327 -0
  81. netizen_cli/quoted_context.py +312 -0
  82. netizen_cli/resources/config.example.yaml +35 -0
  83. netizen_cli/resources/skills/netizen-lark/SKILL.md +64 -0
  84. netizen_cli/resources/skills/netizen-user-guide/SKILL.md +37 -0
  85. netizen_cli/resources/skills/netizen-user-guide/references/user-guide.md +842 -0
  86. netizen_cli/result_images.py +123 -0
  87. netizen_cli/runtime/__init__.py +1 -0
  88. netizen_cli/runtime/contracts.py +792 -0
  89. netizen_cli/runtime/name_writes.py +67 -0
  90. netizen_cli/runtime/thread_naming.py +451 -0
  91. netizen_cli/schedules/__init__.py +1 -0
  92. netizen_cli/schedules/mcp.py +535 -0
  93. netizen_cli/schedules/models.py +394 -0
  94. netizen_cli/schedules/scheduler.py +374 -0
  95. netizen_cli/schedules/service.py +766 -0
  96. netizen_cli/schedules/store.py +771 -0
  97. netizen_cli/sdk_gap_adapter.py +1151 -0
  98. netizen_cli/service_launcher.py +583 -0
  99. netizen_cli/session_settings.py +126 -0
  100. netizen_cli/settings.py +216 -0
  101. netizen_cli/skill_references.py +40 -0
  102. netizen_cli/terminal_cleanup.py +155 -0
  103. netizen_cli/turn_activity.py +688 -0
  104. netizen_cli/turn_files.py +812 -0
  105. netizen_cli/turn_patch_children.py +254 -0
  106. netizen_cli/turn_plan_observer.py +315 -0
  107. netizen_cli/user_questions.py +106 -0
  108. netizen_cli-0.10.0.dist-info/METADATA +18 -0
  109. netizen_cli-0.10.0.dist-info/RECORD +112 -0
  110. netizen_cli-0.10.0.dist-info/WHEEL +5 -0
  111. netizen_cli-0.10.0.dist-info/entry_points.txt +2 -0
  112. 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