@agentsdock/server 1.0.7-beta.5

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 (113) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +19 -0
  3. package/README.md +60 -0
  4. package/npm/cli.cjs +289 -0
  5. package/package.json +32 -0
  6. package/server/LICENSE +202 -0
  7. package/server/NOTICE +19 -0
  8. package/server/VERSION +1 -0
  9. package/server/activation_transaction.py +2458 -0
  10. package/server/agent_server.py +93226 -0
  11. package/server/agentsdock_chats.py +1237 -0
  12. package/server/agentsdock_emergency.py +338 -0
  13. package/server/agentsdock_jobs.py +640 -0
  14. package/server/agentsdock_mail.py +325 -0
  15. package/server/agentsdock_publish.py +404 -0
  16. package/server/agentsdock_team.py +793 -0
  17. package/server/agentsdock_team_hub/__init__.py +33 -0
  18. package/server/agentsdock_team_hub/auth.py +710 -0
  19. package/server/agentsdock_team_hub/cli.py +647 -0
  20. package/server/agentsdock_team_hub/database.py +310 -0
  21. package/server/agentsdock_team_hub/mail_hint_streams.py +176 -0
  22. package/server/agentsdock_team_hub/mail_hints.py +201 -0
  23. package/server/agentsdock_team_hub/migrations/0001_identity_auth.sql +690 -0
  24. package/server/agentsdock_team_hub/migrations/0002_teamspace_ledger.sql +1336 -0
  25. package/server/agentsdock_team_hub/migrations/0003_service_runtime.sql +361 -0
  26. package/server/agentsdock_team_hub/migrations/0004_managed_host_binding.sql +22 -0
  27. package/server/agentsdock_team_hub/migrations/0005_tailnet_bootstrap_delegations.sql +63 -0
  28. package/server/agentsdock_team_hub/migrations/0006_team_network_mailbox.sql +423 -0
  29. package/server/agentsdock_team_hub/migrations/0007_local_agent_mail.sql +63 -0
  30. package/server/agentsdock_team_hub/migrations/0008_managed_server_session.sql +73 -0
  31. package/server/agentsdock_team_hub/migrations/0009_team_messages.sql +324 -0
  32. package/server/agentsdock_team_hub/migrations/0010_team_attachment_orphan_reclamation.sql +52 -0
  33. package/server/agentsdock_team_hub/migrations/0011_human_admin_paging.sql +65 -0
  34. package/server/agentsdock_team_hub/migrations/0012_network_content_deletions.sql +69 -0
  35. package/server/agentsdock_team_hub/migrations/0013_team_message_revisions.sql +48 -0
  36. package/server/agentsdock_team_hub/migrations/0014_managed_network_owner.sql +50 -0
  37. package/server/agentsdock_team_hub/migrations/0015_team_message_inbox_dismissals.sql +3 -0
  38. package/server/agentsdock_team_hub/migrations/0016_team_message_all_servers.sql +4 -0
  39. package/server/agentsdock_team_hub/migrations/0017_skill_announcement_deletions.sql +16 -0
  40. package/server/agentsdock_team_hub/migrations/0018_team_mail_subjects.sql +8 -0
  41. package/server/agentsdock_team_hub/migrations/0019_team_mailbox_state.sql +5 -0
  42. package/server/agentsdock_team_hub/migrations/0020_team_mail_arrivals.sql +44 -0
  43. package/server/agentsdock_team_hub/migrations/0021_team_mail_threads.sql +4 -0
  44. package/server/agentsdock_team_hub/migrations/0022_team_bulletin_changes.sql +49 -0
  45. package/server/agentsdock_team_hub/migrations/0023_team_message_search.sql +93 -0
  46. package/server/agentsdock_team_hub/migrations/__init__.py +1 -0
  47. package/server/agentsdock_team_hub/notification_hints.py +306 -0
  48. package/server/agentsdock_team_hub/secure_peer.py +11215 -0
  49. package/server/agentsdock_team_hub/secure_peer_hub.py +1372 -0
  50. package/server/agentsdock_team_hub/security.py +318 -0
  51. package/server/agentsdock_team_hub/service.py +1737 -0
  52. package/server/agentsdock_team_hub/store.py +16276 -0
  53. package/server/chat_mailbox.py +448 -0
  54. package/server/claude_background_reconciliation.py +92 -0
  55. package/server/claude_goals.py +245 -0
  56. package/server/claude_history_provenance.py +137 -0
  57. package/server/claude_history_repair.py +1601 -0
  58. package/server/claude_sdk_client.py +3731 -0
  59. package/server/claude_side_question.py +212 -0
  60. package/server/codex_app_server.py +3030 -0
  61. package/server/codex_auth.py +93 -0
  62. package/server/codex_history_repair.py +1247 -0
  63. package/server/codex_provider.py +1120 -0
  64. package/server/codex_side_question.py +301 -0
  65. package/server/cursor_agent_client.py +654 -0
  66. package/server/cursor_process_guard.py +347 -0
  67. package/server/execution_activation.py +610 -0
  68. package/server/execution_control.py +233 -0
  69. package/server/execution_durability.py +184 -0
  70. package/server/execution_http.py +256 -0
  71. package/server/execution_install.py +792 -0
  72. package/server/execution_legacy_runner.py +406 -0
  73. package/server/execution_maintenance.py +137 -0
  74. package/server/execution_manage.py +615 -0
  75. package/server/execution_ownership.py +83 -0
  76. package/server/execution_preparation.py +362 -0
  77. package/server/execution_recovery.py +845 -0
  78. package/server/execution_recovery_status.py +195 -0
  79. package/server/execution_service.py +387 -0
  80. package/server/execution_transport.py +622 -0
  81. package/server/execution_uninstall.py +406 -0
  82. package/server/execution_update_status.py +49 -0
  83. package/server/install.sh +7074 -0
  84. package/server/interactive_chat_controls.py +153 -0
  85. package/server/interactive_chat_native.py +66 -0
  86. package/server/interactive_chat_projection.py +152 -0
  87. package/server/interactive_chat_runtime.py +132 -0
  88. package/server/interactive_chat_share_routes.py +534 -0
  89. package/server/interactive_chat_share_web.py +12 -0
  90. package/server/interactive_chat_shares.py +238 -0
  91. package/server/provider_commands.py +425 -0
  92. package/server/public_chat_share_routes.py +426 -0
  93. package/server/public_chat_shares.py +759 -0
  94. package/server/public_chat_transcript.py +357 -0
  95. package/server/pyproject.toml +29 -0
  96. package/server/release-public-key.pem +3 -0
  97. package/server/secure_peer_delivery.py +1236 -0
  98. package/server/secure_peer_runtime.py +7694 -0
  99. package/server/shared_chat_video_stream.py +262 -0
  100. package/server/shared_chat_videos.py +155 -0
  101. package/server/side_questions.py +594 -0
  102. package/server/team_hub_host.py +1354 -0
  103. package/server/team_mail_grants.py +136 -0
  104. package/server/team_mail_runtime.py +302 -0
  105. package/server/team_mail_websocket.py +211 -0
  106. package/server/title_generation.py +333 -0
  107. package/server/uninstall.sh +359 -0
  108. package/server/update_handoff.py +237 -0
  109. package/server/update_preparation.py +264 -0
  110. package/server/update_recovery.py +140 -0
  111. package/server/update_runner.py +1819 -0
  112. package/server/uv.lock +1101 -0
  113. package/server/workspace_git.py +545 -0
@@ -0,0 +1,448 @@
1
+ """Passive same-server inbox metadata on the existing cross-chat SQLite ledger.
2
+
3
+ Call mutations in the caller's BEGIN IMMEDIATE transaction and current pair
4
+ authorization lock. This module never commits, opens a database, starts a turn,
5
+ reads credentials, sends a reply, or interprets message text as permission.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ import sqlite3
12
+ import uuid
13
+ from collections.abc import Iterable
14
+
15
+
16
+ MAX_PAGE_ITEMS = 25
17
+ MAX_PAGE_BYTES = 112 * 1024
18
+ _PAIR = re.compile(r"pair_[0-9a-f]{32}\Z")
19
+
20
+
21
+ class MailboxConflict(ValueError):
22
+ pass
23
+
24
+
25
+ def _transaction(connection: sqlite3.Connection) -> None:
26
+ if not connection.in_transaction:
27
+ raise RuntimeError("Mailbox mutations require a caller-owned transaction")
28
+
29
+
30
+ def _identifier(value: str) -> str:
31
+ if not isinstance(value, str) or not value or len(value) > 240 or not value.isprintable():
32
+ raise ValueError("Invalid mailbox identifier")
33
+ return value
34
+
35
+
36
+ def _limit(value: int) -> int:
37
+ if type(value) is not int or not 1 <= value <= MAX_PAGE_ITEMS:
38
+ raise ValueError("Mailbox page limit must be between 1 and 25")
39
+ return value
40
+
41
+
42
+ def _cursor(value: int) -> int:
43
+ if type(value) is not int or not 0 <= value <= (1 << 63) - 1:
44
+ raise ValueError("Invalid mailbox cursor")
45
+ return value
46
+
47
+
48
+ def _pairs(values: Iterable[str]) -> str:
49
+ if isinstance(values, (str, bytes)):
50
+ raise ValueError("Expected current authorized pairs")
51
+ clean = set(values)
52
+ if any(not isinstance(value, str) or not _PAIR.fullmatch(value) for value in clean):
53
+ raise ValueError("Invalid authorized pair")
54
+ return json.dumps(sorted(clean), separators=(",", ":"))
55
+
56
+
57
+ def initialize(connection: sqlite3.Connection) -> None:
58
+ """Add mailbox tables without altering existing envelopes or their bodies."""
59
+ statements = (
60
+ """CREATE TABLE IF NOT EXISTS chat_mailbox_reads (
61
+ id TEXT PRIMARY KEY, target_session_id TEXT NOT NULL,
62
+ source_session_id TEXT NOT NULL, reader_run_id TEXT NOT NULL,
63
+ request_id TEXT NOT NULL, snapshot_seq INTEGER NOT NULL,
64
+ page_limit INTEGER NOT NULL, created_at TEXT NOT NULL,
65
+ UNIQUE(target_session_id, reader_run_id, request_id))""",
66
+ """CREATE TABLE IF NOT EXISTS chat_mailbox_messages (
67
+ mailbox_seq INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ message_id TEXT NOT NULL UNIQUE REFERENCES cross_chat_envelopes(id) ON DELETE CASCADE,
69
+ source_session_id TEXT NOT NULL, target_session_id TEXT NOT NULL,
70
+ pair_id TEXT NOT NULL, stored_at TEXT NOT NULL,
71
+ in_reply_to_message_id TEXT, read_id TEXT REFERENCES chat_mailbox_reads(id),
72
+ read_at TEXT, excluded_at TEXT, excluded_reason TEXT,
73
+ read_event_published INTEGER NOT NULL DEFAULT 0 CHECK(read_event_published IN (0,1)))""",
74
+ """CREATE TABLE IF NOT EXISTS chat_mailbox_read_pages (
75
+ read_id TEXT NOT NULL REFERENCES chat_mailbox_reads(id) ON DELETE CASCADE,
76
+ after_seq INTEGER NOT NULL, end_seq INTEGER NOT NULL,
77
+ has_more INTEGER NOT NULL CHECK(has_more IN (0,1)), message_ids_json TEXT NOT NULL,
78
+ PRIMARY KEY(read_id, after_seq))""",
79
+ """CREATE TABLE IF NOT EXISTS chat_mailbox_wakes (
80
+ target_session_id TEXT PRIMARY KEY,
81
+ last_attempt_seq INTEGER NOT NULL DEFAULT 0 CHECK(last_attempt_seq>=0),
82
+ suppressed_seq INTEGER NOT NULL DEFAULT 0 CHECK(suppressed_seq>=0),
83
+ claim_id TEXT UNIQUE, through_seq INTEGER NOT NULL DEFAULT 0 CHECK(through_seq>=0),
84
+ state TEXT CHECK(state IN ('reserved','admitted')), run_id TEXT,
85
+ updated_at TEXT NOT NULL)""",
86
+ """CREATE INDEX IF NOT EXISTS chat_mailbox_sender_order
87
+ ON chat_mailbox_messages(target_session_id, source_session_id, mailbox_seq)""",
88
+ """CREATE INDEX IF NOT EXISTS chat_mailbox_unread_order
89
+ ON chat_mailbox_messages(target_session_id, read_at, mailbox_seq)""",
90
+ """CREATE INDEX IF NOT EXISTS chat_mailbox_read_outbox
91
+ ON chat_mailbox_messages(read_event_published, mailbox_seq)
92
+ WHERE read_at IS NOT NULL AND excluded_at IS NULL""",
93
+ """CREATE INDEX IF NOT EXISTS chat_mailbox_read_request
94
+ ON chat_mailbox_reads(target_session_id, request_id)""",
95
+ )
96
+ for statement in statements:
97
+ connection.execute(statement)
98
+
99
+
100
+ _JOIN = "chat_mailbox_messages m JOIN cross_chat_envelopes e ON e.id=m.message_id"
101
+ _VALID = """m.excluded_at IS NULL AND e.status='stored' AND e.delivery_mode='mailbox'
102
+ AND e.kind='instruction' AND e.authorization_kind='configured_route'
103
+ AND e.source_session_id=m.source_session_id AND e.target_session_id=m.target_session_id
104
+ AND e.authorization_pair_id=m.pair_id"""
105
+ _COLUMNS = """m.*, e.body AS original_body, e.target_body, e.message_revision, e.created_at,
106
+ e.source_user_instruction"""
107
+
108
+
109
+ def _message(row: sqlite3.Row) -> dict:
110
+ edited = row["message_revision"] > 0 and isinstance(row["target_body"], str)
111
+ return {
112
+ "message_id": row["message_id"], "mailbox_seq": row["mailbox_seq"],
113
+ "source_session_id": row["source_session_id"], "target_session_id": row["target_session_id"],
114
+ "conversation_id": row["pair_id"],
115
+ "body": row["target_body"] if edited else row["original_body"],
116
+ "source_user_instruction": row["source_user_instruction"] or "",
117
+ "created_at": row["created_at"], "stored_at": row["stored_at"],
118
+ "in_reply_to_message_id": row["in_reply_to_message_id"],
119
+ "read_id": row["read_id"], "read_at": row["read_at"],
120
+ "message_revision": row["message_revision"] if edited else 0,
121
+ "message_edited_by_user": edited,
122
+ }
123
+
124
+
125
+ def store_message(connection: sqlite3.Connection, message_id: str, *, now: str,
126
+ in_reply_to_message_id: str | None = None) -> dict:
127
+ """Register one already-authorized stored envelope in the same transaction."""
128
+ _transaction(connection)
129
+ record = connection.execute("SELECT * FROM cross_chat_envelopes WHERE id=?",
130
+ (_identifier(message_id),)).fetchone()
131
+ if (record is None or record["status"] != "stored" or record["delivery_mode"] != "mailbox"
132
+ or record["kind"] != "instruction" or record["authorization_kind"] != "configured_route"
133
+ or not _PAIR.fullmatch(record["authorization_pair_id"] or "")
134
+ or record["source_session_id"] == record["target_session_id"]
135
+ or record["target_run_id"]):
136
+ raise MailboxConflict("Envelope is not an unlaunched mailbox message")
137
+ parent = record["reply_to_message_id"]
138
+ if in_reply_to_message_id is not None and in_reply_to_message_id != parent:
139
+ raise MailboxConflict("Reply relationship does not match the stored envelope")
140
+ if parent:
141
+ prior = connection.execute("SELECT * FROM cross_chat_envelopes WHERE id=?", (parent,)).fetchone()
142
+ prior_mail = connection.execute("SELECT excluded_at FROM chat_mailbox_messages WHERE message_id=?",
143
+ (parent,)).fetchone()
144
+ if (prior is None or prior["source_session_id"] != record["target_session_id"]
145
+ or prior["target_session_id"] != record["source_session_id"]
146
+ or prior["authorization_pair_id"] != record["authorization_pair_id"]
147
+ or prior["status"] in ("failed", "cancelled")
148
+ or (prior_mail is not None and prior_mail["excluded_at"] is not None)):
149
+ raise MailboxConflict("Reply must reference an available exact peer message")
150
+ connection.execute("""INSERT OR IGNORE INTO chat_mailbox_messages
151
+ (message_id,source_session_id,target_session_id,pair_id,stored_at,in_reply_to_message_id)
152
+ VALUES(?,?,?,?,?,?)""", (message_id, record["source_session_id"], record["target_session_id"],
153
+ record["authorization_pair_id"], now, parent))
154
+ row = connection.execute(f"SELECT {_COLUMNS} FROM {_JOIN} WHERE m.message_id=?", (message_id,)).fetchone()
155
+ if (row["source_session_id"] != record["source_session_id"]
156
+ or row["target_session_id"] != record["target_session_id"]
157
+ or row["pair_id"] != record["authorization_pair_id"]
158
+ or row["in_reply_to_message_id"] != parent):
159
+ raise MailboxConflict("Stored mailbox identity changed")
160
+ message = _message(row)
161
+ # Reject before the caller commits, rather than accepting mail that no
162
+ # bounded read can deliver. Include the metadata added on its first read.
163
+ readable = {**message, "read_id": "mailread_" + "0" * 32, "read_at": now}
164
+ if 8192 + len(json.dumps(readable, ensure_ascii=False, separators=(",", ":")).encode("utf-8")) > MAX_PAGE_BYTES:
165
+ raise MailboxConflict("Message and source user instruction exceed the bounded mailbox response")
166
+ return message
167
+
168
+
169
+ def _scope(target: str, source: str | None, pairs: Iterable[str]) -> tuple[str, list]:
170
+ clause = f"{_VALID} AND m.target_session_id=? AND m.pair_id IN (SELECT value FROM json_each(?))"
171
+ args = [_identifier(target), _pairs(pairs)]
172
+ if source:
173
+ clause += " AND m.source_session_id=?"
174
+ args.append(_identifier(source))
175
+ return clause, args
176
+
177
+
178
+ def list_senders(connection: sqlite3.Connection, target_session_id: str, allowed_pair_ids: Iterable[str],
179
+ *, after_sender: str = "", limit: int = MAX_PAGE_ITEMS, unread_only: bool = False) -> dict:
180
+ """Read grouped metadata only; opening the mailbox never marks anything read."""
181
+ clause, args = _scope(target_session_id, None, allowed_pair_ids)
182
+ if after_sender:
183
+ _identifier(after_sender)
184
+ if unread_only:
185
+ clause += " AND m.read_at IS NULL"
186
+ rows = connection.execute(f"""SELECT m.source_session_id, COUNT(*) AS message_count,
187
+ SUM(CASE WHEN m.read_at IS NULL THEN 1 ELSE 0 END) AS unread_count,
188
+ MAX(m.mailbox_seq) AS latest_seq, MAX(e.created_at) AS latest_created_at
189
+ FROM {_JOIN} WHERE {clause} AND m.source_session_id>?
190
+ GROUP BY m.source_session_id ORDER BY m.source_session_id LIMIT ?""",
191
+ (*args, after_sender, _limit(limit) + 1)).fetchall()
192
+ more = len(rows) > limit
193
+ selected = [dict(row) for row in rows[:limit]]
194
+ return {"senders": selected, "has_more": more,
195
+ "next_after_sender": selected[-1]["source_session_id"] if more else None}
196
+
197
+
198
+ def _page(connection: sqlite3.Connection, clause: str, args: list, limit: int,
199
+ read_metadata: dict | None = None) -> tuple[list[dict], bool]:
200
+ messages, used = [], 8192 # reserve for public titles, receipt and reply-route metadata
201
+ rows = connection.execute(f"SELECT {_COLUMNS} FROM {_JOIN} WHERE {clause} ORDER BY m.mailbox_seq LIMIT ?",
202
+ (*args, limit + 1))
203
+ for row in rows:
204
+ message = _message(row)
205
+ if read_metadata:
206
+ message.update(read_metadata)
207
+ size = len(json.dumps(message, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))
208
+ if len(messages) == limit or used + size > MAX_PAGE_BYTES:
209
+ if not messages:
210
+ raise MailboxConflict("Message exceeds the bounded mailbox response; no message was read")
211
+ return messages, True
212
+ messages.append(message)
213
+ used += size
214
+ return messages, False
215
+
216
+
217
+ def list_messages(connection: sqlite3.Connection, target_session_id: str, source_session_id: str | None,
218
+ allowed_pair_ids: Iterable[str], *, after_seq: int = 0, limit: int = MAX_PAGE_ITEMS,
219
+ unread_only: bool = False) -> dict:
220
+ clause, args = _scope(target_session_id, source_session_id, allowed_pair_ids)
221
+ clause += " AND m.mailbox_seq>?"
222
+ args.append(_cursor(after_seq))
223
+ if unread_only:
224
+ clause += " AND m.read_at IS NULL"
225
+ messages, more = _page(connection, clause, args, _limit(limit))
226
+ return {"messages": messages, "has_more": more,
227
+ "next_after_seq": messages[-1]["mailbox_seq"] if more else None}
228
+
229
+
230
+ def read_sender(connection: sqlite3.Connection, *, target_session_id: str, source_session_id: str,
231
+ reader_run_id: str, request_id: str, allowed_pair_ids: Iterable[str], now: str,
232
+ after_seq: int = 0, limit: int = MAX_PAGE_ITEMS) -> dict:
233
+ """Atomically read one page of a fixed unread snapshot, with retry receipts.
234
+
235
+ Arrivals after snapshot_seq remain unread. A retry only returns its original
236
+ page IDs across fresh authorized runs, subject to current authorization and
237
+ cancellation/deletion state. The first reader run is attribution, not authority.
238
+ Read means included in this tool response, not proof of model understanding.
239
+ """
240
+ _transaction(connection)
241
+ _identifier(source_session_id)
242
+ _identifier(reader_run_id)
243
+ _identifier(request_id)
244
+ _cursor(after_seq)
245
+ _limit(limit)
246
+ clause, args = _scope(target_session_id, source_session_id, allowed_pair_ids)
247
+ batches = connection.execute("""SELECT * FROM chat_mailbox_reads
248
+ WHERE target_session_id=? AND request_id=? LIMIT 2""",
249
+ (target_session_id, request_id)).fetchall()
250
+ if len(batches) > 1:
251
+ # Older versions permitted this key once per run. Never guess which
252
+ # historical snapshot a caller intended, or consume another batch.
253
+ raise MailboxConflict("Read request matches multiple historical snapshots")
254
+ batch = batches[0] if batches else None
255
+ if batch is None:
256
+ if after_seq:
257
+ raise MailboxConflict("A mailbox read must start at its first page")
258
+ high_water = connection.execute("""SELECT COALESCE(MAX(mailbox_seq),0) FROM chat_mailbox_messages
259
+ WHERE target_session_id=? AND source_session_id=?""", (target_session_id, source_session_id)).fetchone()[0]
260
+ read_id = "mailread_" + uuid.uuid4().hex
261
+ connection.execute("""INSERT INTO chat_mailbox_reads
262
+ (id,target_session_id,source_session_id,reader_run_id,request_id,snapshot_seq,page_limit,created_at)
263
+ VALUES(?,?,?,?,?,?,?,?)""", (read_id, target_session_id, source_session_id, reader_run_id,
264
+ request_id, high_water, limit, now))
265
+ batch = connection.execute("SELECT * FROM chat_mailbox_reads WHERE id=?", (read_id,)).fetchone()
266
+ if batch["source_session_id"] != source_session_id or batch["page_limit"] != limit:
267
+ raise MailboxConflict("Read request was already used with different parameters")
268
+ page = connection.execute("SELECT * FROM chat_mailbox_read_pages WHERE read_id=? AND after_seq=?",
269
+ (batch["id"], after_seq)).fetchone()
270
+ replayed = page is not None
271
+ if page is None:
272
+ if after_seq and connection.execute("""SELECT 1 FROM chat_mailbox_read_pages
273
+ WHERE read_id=? AND end_seq=? AND has_more=1""", (batch["id"], after_seq)).fetchone() is None:
274
+ raise MailboxConflict("Cursor does not belong to this read snapshot")
275
+ messages, more = _page(connection, clause + " AND m.read_at IS NULL AND m.mailbox_seq>? AND m.mailbox_seq<=?",
276
+ [*args, after_seq, batch["snapshot_seq"]], limit,
277
+ {"read_id": batch["id"], "read_at": now})
278
+ ids = [message["message_id"] for message in messages]
279
+ end = messages[-1]["mailbox_seq"] if messages else after_seq
280
+ connection.execute("""INSERT INTO chat_mailbox_read_pages
281
+ (read_id,after_seq,end_seq,has_more,message_ids_json) VALUES(?,?,?,?,?)""",
282
+ (batch["id"], after_seq, end, int(more), json.dumps(ids, separators=(",", ":"))))
283
+ for message_id in ids:
284
+ connection.execute("""UPDATE chat_mailbox_messages SET read_id=?,read_at=?
285
+ WHERE message_id=? AND read_at IS NULL""", (batch["id"], now, message_id))
286
+ page = connection.execute("SELECT * FROM chat_mailbox_read_pages WHERE read_id=? AND after_seq=?",
287
+ (batch["id"], after_seq)).fetchone()
288
+ ids = json.loads(page["message_ids_json"])
289
+ messages, overflow = _page(connection, clause + " AND m.message_id IN (SELECT value FROM json_each(?))",
290
+ [*args, json.dumps(ids)], limit) if ids else ([], False)
291
+ if overflow:
292
+ raise MailboxConflict("Stored mailbox read page exceeds the bounded response; retry was not truncated")
293
+ return {"read_id": batch["id"], "snapshot_seq": batch["snapshot_seq"], "messages": messages,
294
+ "next_after_seq": page["end_seq"] if page["has_more"] else None,
295
+ "has_more": bool(page["has_more"]), "unavailable_count": len(ids) - len(messages), "replayed": replayed}
296
+
297
+
298
+ def exclude_message(connection: sqlite3.Connection, message_id: str, *, target_session_id: str,
299
+ reason: str, now: str) -> bool:
300
+ _transaction(connection)
301
+ if reason not in ("cancelled", "deleted", "revoked"):
302
+ raise ValueError("Invalid mailbox exclusion reason")
303
+ return connection.execute("""UPDATE chat_mailbox_messages SET excluded_at=?,excluded_reason=?
304
+ WHERE message_id=? AND target_session_id=? AND excluded_at IS NULL""",
305
+ (now, reason, _identifier(message_id), _identifier(target_session_id))).rowcount == 1
306
+
307
+
308
+ def cancel_message(connection: sqlite3.Connection, message_id: str, *, now: str) -> bool:
309
+ """Cancel an unread stored message atomically; a completed read wins the race."""
310
+ _transaction(connection)
311
+ record = connection.execute(f"""SELECT m.read_at,m.excluded_at,m.excluded_reason,
312
+ e.status,e.delivery_mode FROM {_JOIN} WHERE m.message_id=?""", (_identifier(message_id),)).fetchone()
313
+ if record is None:
314
+ return False
315
+ if record["status"] == "cancelled" and record["excluded_reason"] == "cancelled":
316
+ return False
317
+ if (record["status"] != "stored" or record["delivery_mode"] != "mailbox"
318
+ or record["excluded_at"] is not None or record["read_at"] is not None):
319
+ raise MailboxConflict("Message is no longer unread and cancellable")
320
+ connection.execute("""UPDATE chat_mailbox_messages SET excluded_at=?,excluded_reason='cancelled'
321
+ WHERE message_id=?""", (now, message_id))
322
+ connection.execute("""UPDATE cross_chat_envelopes SET status='cancelled',lifecycle_status='',updated_at=?
323
+ WHERE id=?""", (now, message_id))
324
+ return True
325
+
326
+
327
+ def exclude_pair(connection: sqlite3.Connection, pair_id: str, *, now: str) -> int:
328
+ _transaction(connection)
329
+ _pairs([pair_id])
330
+ return connection.execute("""UPDATE chat_mailbox_messages SET excluded_at=?,excluded_reason='revoked'
331
+ WHERE pair_id=? AND excluded_at IS NULL""", (now, pair_id)).rowcount
332
+
333
+
334
+ def exclude_session(connection: sqlite3.Connection, session_id: str, *, now: str) -> int:
335
+ _transaction(connection)
336
+ _identifier(session_id)
337
+ return connection.execute("""UPDATE chat_mailbox_messages SET excluded_at=?,excluded_reason='deleted'
338
+ WHERE (source_session_id=? OR target_session_id=?) AND excluded_at IS NULL""",
339
+ (now, session_id, session_id)).rowcount
340
+
341
+
342
+ def pending_read_events(connection: sqlite3.Connection, *, limit: int = MAX_PAGE_ITEMS) -> list[dict]:
343
+ rows, _ = _page(connection, _VALID + " AND m.read_at IS NOT NULL AND m.read_event_published=0", [], _limit(limit))
344
+ return rows
345
+
346
+
347
+ def unread_targets(connection: sqlite3.Connection) -> list[str]:
348
+ """Startup recovery metadata only; never fetch bodies or launch recipients."""
349
+ return [row[0] for row in connection.execute(f"""SELECT DISTINCT m.target_session_id FROM {_JOIN}
350
+ WHERE {_VALID} AND m.read_at IS NULL ORDER BY m.target_session_id""")]
351
+
352
+
353
+ def mark_read_event_published(connection: sqlite3.Connection, message_id: str) -> bool:
354
+ _transaction(connection)
355
+ return connection.execute("""UPDATE chat_mailbox_messages SET read_event_published=1
356
+ WHERE message_id=? AND read_at IS NOT NULL AND read_event_published=0""",
357
+ (_identifier(message_id),)).rowcount == 1
358
+
359
+
360
+ def claim_wake(connection: sqlite3.Connection, target_session_id: str,
361
+ allowed_pair_ids: Iterable[str], *, now: str) -> dict | None:
362
+ """Reserve one authorized unread cutoff; the caller must separately prove idle.
363
+
364
+ A wake is an execution attempt, not a read receipt. New arrivals can replace
365
+ an admitted attempt only when the runtime has again proved the chat idle.
366
+ """
367
+ _transaction(connection)
368
+ clause, args = _scope(target_session_id, None, allowed_pair_ids)
369
+ prior = connection.execute("SELECT * FROM chat_mailbox_wakes WHERE target_session_id=?",
370
+ (target_session_id,)).fetchone()
371
+ if prior is not None and prior["state"] == "reserved":
372
+ return None
373
+ cutoff = max(prior["last_attempt_seq"], prior["suppressed_seq"]) if prior else 0
374
+ through_seq = connection.execute(f"""SELECT COALESCE(MAX(m.mailbox_seq),0) FROM {_JOIN}
375
+ WHERE {clause} AND m.read_at IS NULL AND m.mailbox_seq>?""", (*args, cutoff)).fetchone()[0]
376
+ if not through_seq:
377
+ return None
378
+ claim_id = "mailwake_" + uuid.uuid4().hex
379
+ connection.execute("""INSERT INTO chat_mailbox_wakes
380
+ (target_session_id,claim_id,through_seq,state,updated_at) VALUES(?,?,?,'reserved',?)
381
+ ON CONFLICT(target_session_id) DO UPDATE SET claim_id=excluded.claim_id,
382
+ through_seq=excluded.through_seq,state='reserved',run_id=NULL,updated_at=excluded.updated_at""",
383
+ (target_session_id, claim_id, through_seq, now))
384
+ return dict(connection.execute("SELECT * FROM chat_mailbox_wakes WHERE target_session_id=?",
385
+ (target_session_id,)).fetchone())
386
+
387
+
388
+ def admit_wake(connection: sqlite3.Connection, target_session_id: str, claim_id: str,
389
+ run_id: str, allowed_pair_ids: Iterable[str], *, now: str) -> bool:
390
+ """Consume one reservation before provider launch, retaining ambiguous attempts.
391
+
392
+ Current route permission and unread state are rechecked within the claimed
393
+ cutoff. Even an identical repeated admission returns False: it is not a
394
+ second authorization to launch the provider.
395
+ """
396
+ _transaction(connection)
397
+ clause, args = _scope(target_session_id, None, allowed_pair_ids)
398
+ _identifier(claim_id)
399
+ _identifier(run_id)
400
+ claim = connection.execute("""SELECT * FROM chat_mailbox_wakes
401
+ WHERE target_session_id=? AND claim_id=? AND state='reserved'""",
402
+ (target_session_id, claim_id)).fetchone()
403
+ if claim is None:
404
+ return False
405
+ eligible = connection.execute(f"""SELECT 1 FROM {_JOIN} WHERE {clause}
406
+ AND m.read_at IS NULL AND m.mailbox_seq>? AND m.mailbox_seq<=? LIMIT 1""",
407
+ (*args, max(claim["last_attempt_seq"], claim["suppressed_seq"]), claim["through_seq"])).fetchone()
408
+ if eligible is None:
409
+ return False
410
+ return connection.execute("""UPDATE chat_mailbox_wakes SET state='admitted',run_id=?,
411
+ last_attempt_seq=MAX(last_attempt_seq,through_seq),updated_at=?
412
+ WHERE target_session_id=? AND claim_id=? AND state='reserved'""",
413
+ (run_id, now, target_session_id, claim_id)).rowcount == 1
414
+
415
+
416
+ def release_wake(connection: sqlite3.Connection, target_session_id: str, claim_id: str) -> bool:
417
+ """Release only a known unlaunched reservation, never an admitted attempt."""
418
+ _transaction(connection)
419
+ return connection.execute("""UPDATE chat_mailbox_wakes
420
+ SET claim_id=NULL,through_seq=0,state=NULL,run_id=NULL
421
+ WHERE target_session_id=? AND claim_id=? AND state='reserved'""",
422
+ (_identifier(target_session_id), _identifier(claim_id))).rowcount == 1
423
+
424
+
425
+ def suppress_wake(connection: sqlite3.Connection, target_session_id: str, *, now: str) -> int:
426
+ """Stop suppresses current unread mail, without reading it or future arrivals."""
427
+ _transaction(connection)
428
+ target_session_id = _identifier(target_session_id)
429
+ through_seq = connection.execute(f"""SELECT COALESCE(MAX(m.mailbox_seq),0) FROM {_JOIN}
430
+ WHERE {_VALID} AND m.target_session_id=? AND m.read_at IS NULL""",
431
+ (target_session_id,)).fetchone()[0]
432
+ connection.execute("""INSERT INTO chat_mailbox_wakes
433
+ (target_session_id,suppressed_seq,updated_at) VALUES(?,?,?)
434
+ ON CONFLICT(target_session_id) DO UPDATE
435
+ SET suppressed_seq=MAX(suppressed_seq,excluded.suppressed_seq),updated_at=excluded.updated_at""",
436
+ (target_session_id, through_seq, now))
437
+ connection.execute("""UPDATE chat_mailbox_wakes
438
+ SET claim_id=NULL,through_seq=0,state=NULL,run_id=NULL
439
+ WHERE target_session_id=? AND state='reserved'""", (target_session_id,))
440
+ return connection.execute("SELECT suppressed_seq FROM chat_mailbox_wakes WHERE target_session_id=?",
441
+ (target_session_id,)).fetchone()[0]
442
+
443
+
444
+ def recover_wakes(connection: sqlite3.Connection) -> int:
445
+ """Startup only: no provider may launch before its reservation is admitted."""
446
+ _transaction(connection)
447
+ return connection.execute("""UPDATE chat_mailbox_wakes
448
+ SET claim_id=NULL,through_seq=0,state=NULL,run_id=NULL WHERE state='reserved'""").rowcount
@@ -0,0 +1,92 @@
1
+ """Bounded, data-only Claude task receipts; no I/O or provider transcripts."""
2
+ from __future__ import annotations
3
+
4
+ from collections import OrderedDict
5
+ from collections.abc import Mapping
6
+ import json
7
+
8
+
9
+ RECONCILED_EVENT = "claude_background_tasks_reconciled"
10
+ CONSUMED_EVENT = "claude_background_task_reconciliation_consumed"
11
+ MAX_TASKS = 64
12
+ MAX_TASK_BYTES = 6144
13
+ SESSION_FIELD = "claude_background_reconciliation"
14
+ STATUSES = frozenset({"running", "completed", "failed", "stopped", "killed", "tracking_lost"})
15
+
16
+
17
+ def _identifier(value, limit=256):
18
+ return value if isinstance(value, str) and 0 < len(value) <= limit and all(
19
+ ord(char) >= 32 and ord(char) != 127 for char in value
20
+ ) else None
21
+
22
+
23
+ def normalize_task_receipts(receipts, *, run_id, provider_session_id, tracking_lost=False):
24
+ """Allowlist exact task ownership and statuses, never commands or summaries."""
25
+ if not _identifier(run_id) or not _identifier(provider_session_id):
26
+ return []
27
+ if not isinstance(receipts, (list, tuple)):
28
+ return []
29
+ result = OrderedDict()
30
+ for receipt in receipts[:MAX_TASKS]:
31
+ if not isinstance(receipt, Mapping):
32
+ continue
33
+ task_id = _identifier(receipt.get("task_id"))
34
+ status = receipt.get("status")
35
+ task_type = _identifier(receipt.get("task_type"), 64)
36
+ if not task_id or not isinstance(status, str) or status not in STATUSES or not task_type:
37
+ continue
38
+ if receipt.get("owner_run_id") != run_id or receipt.get("provider_session_id") not in (None, "", provider_session_id):
39
+ continue
40
+ task = {
41
+ "task_id": task_id, "task_type": task_type,
42
+ "status": "tracking_lost" if tracking_lost and status == "running" else status,
43
+ "owner_run_id": run_id, "provider_session_id": provider_session_id,
44
+ }
45
+ tool_id = _identifier(receipt.get("tool_use_id"))
46
+ if tool_id:
47
+ task["tool_use_id"] = tool_id
48
+ result[task_id] = task
49
+ return list(result.values())
50
+
51
+
52
+ def reconciliation_envelope(batches):
53
+ """Latest exact-owner state wins; discarded states are counted, not invented."""
54
+ tasks = OrderedDict()
55
+ overflow = 0
56
+ for batch in batches[-2:]:
57
+ overflow += max(0, min(int(batch.get("overflow_count") or 0), 1_000_000))
58
+ for task in batch.get("tasks", ()):
59
+ key = (task["provider_session_id"], task["owner_run_id"], task["task_id"])
60
+ tasks[key] = dict(task)
61
+ tasks.move_to_end(key)
62
+ selected = list(tasks.values())
63
+ while len(selected) > MAX_TASKS or len(json.dumps(selected, ensure_ascii=True, separators=(",", ":")).encode("utf-8")) > MAX_TASK_BYTES:
64
+ selected.pop(0)
65
+ overflow += 1
66
+ return {"tasks": selected, "overflow_count": min(overflow, 1_000_000)}
67
+
68
+
69
+ def pending_task_reconciliations(value, *, provider_session_id: str):
70
+ """Read a single bounded persisted checkpoint without touching event logs."""
71
+ if not isinstance(value, dict) or value.get("version") != 1 or value.get("provider_session_id") != provider_session_id:
72
+ return []
73
+ identifier = _identifier(value.get("reconciliation_id"))
74
+ overflow = value.get("overflow_count", 0)
75
+ source_tasks = value.get("tasks")
76
+ if not identifier or type(overflow) is not int or overflow < 0 or not isinstance(source_tasks, list):
77
+ return []
78
+ tasks = []
79
+ for receipt in source_tasks[:MAX_TASKS]:
80
+ if isinstance(receipt, dict):
81
+ tasks.extend(normalize_task_receipts([receipt], run_id=receipt.get("owner_run_id"), provider_session_id=provider_session_id))
82
+ envelope = reconciliation_envelope([{"tasks": tasks, "overflow_count": overflow + max(0, len(source_tasks) - MAX_TASKS)}])
83
+ return [{"reconciliation_id": identifier, **envelope}]
84
+
85
+
86
+ def pending_reconciliation_state(previous, batch, *, provider_session_id):
87
+ pending = pending_task_reconciliations(previous, provider_session_id=provider_session_id)
88
+ return {
89
+ "version": 1, "provider_session_id": provider_session_id,
90
+ "reconciliation_id": batch["reconciliation_id"],
91
+ **reconciliation_envelope([*pending, batch]),
92
+ }