@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.
- package/LICENSE +202 -0
- package/NOTICE +19 -0
- package/README.md +60 -0
- package/npm/cli.cjs +289 -0
- package/package.json +32 -0
- package/server/LICENSE +202 -0
- package/server/NOTICE +19 -0
- package/server/VERSION +1 -0
- package/server/activation_transaction.py +2458 -0
- package/server/agent_server.py +93226 -0
- package/server/agentsdock_chats.py +1237 -0
- package/server/agentsdock_emergency.py +338 -0
- package/server/agentsdock_jobs.py +640 -0
- package/server/agentsdock_mail.py +325 -0
- package/server/agentsdock_publish.py +404 -0
- package/server/agentsdock_team.py +793 -0
- package/server/agentsdock_team_hub/__init__.py +33 -0
- package/server/agentsdock_team_hub/auth.py +710 -0
- package/server/agentsdock_team_hub/cli.py +647 -0
- package/server/agentsdock_team_hub/database.py +310 -0
- package/server/agentsdock_team_hub/mail_hint_streams.py +176 -0
- package/server/agentsdock_team_hub/mail_hints.py +201 -0
- package/server/agentsdock_team_hub/migrations/0001_identity_auth.sql +690 -0
- package/server/agentsdock_team_hub/migrations/0002_teamspace_ledger.sql +1336 -0
- package/server/agentsdock_team_hub/migrations/0003_service_runtime.sql +361 -0
- package/server/agentsdock_team_hub/migrations/0004_managed_host_binding.sql +22 -0
- package/server/agentsdock_team_hub/migrations/0005_tailnet_bootstrap_delegations.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0006_team_network_mailbox.sql +423 -0
- package/server/agentsdock_team_hub/migrations/0007_local_agent_mail.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0008_managed_server_session.sql +73 -0
- package/server/agentsdock_team_hub/migrations/0009_team_messages.sql +324 -0
- package/server/agentsdock_team_hub/migrations/0010_team_attachment_orphan_reclamation.sql +52 -0
- package/server/agentsdock_team_hub/migrations/0011_human_admin_paging.sql +65 -0
- package/server/agentsdock_team_hub/migrations/0012_network_content_deletions.sql +69 -0
- package/server/agentsdock_team_hub/migrations/0013_team_message_revisions.sql +48 -0
- package/server/agentsdock_team_hub/migrations/0014_managed_network_owner.sql +50 -0
- package/server/agentsdock_team_hub/migrations/0015_team_message_inbox_dismissals.sql +3 -0
- package/server/agentsdock_team_hub/migrations/0016_team_message_all_servers.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0017_skill_announcement_deletions.sql +16 -0
- package/server/agentsdock_team_hub/migrations/0018_team_mail_subjects.sql +8 -0
- package/server/agentsdock_team_hub/migrations/0019_team_mailbox_state.sql +5 -0
- package/server/agentsdock_team_hub/migrations/0020_team_mail_arrivals.sql +44 -0
- package/server/agentsdock_team_hub/migrations/0021_team_mail_threads.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0022_team_bulletin_changes.sql +49 -0
- package/server/agentsdock_team_hub/migrations/0023_team_message_search.sql +93 -0
- package/server/agentsdock_team_hub/migrations/__init__.py +1 -0
- package/server/agentsdock_team_hub/notification_hints.py +306 -0
- package/server/agentsdock_team_hub/secure_peer.py +11215 -0
- package/server/agentsdock_team_hub/secure_peer_hub.py +1372 -0
- package/server/agentsdock_team_hub/security.py +318 -0
- package/server/agentsdock_team_hub/service.py +1737 -0
- package/server/agentsdock_team_hub/store.py +16276 -0
- package/server/chat_mailbox.py +448 -0
- package/server/claude_background_reconciliation.py +92 -0
- package/server/claude_goals.py +245 -0
- package/server/claude_history_provenance.py +137 -0
- package/server/claude_history_repair.py +1601 -0
- package/server/claude_sdk_client.py +3731 -0
- package/server/claude_side_question.py +212 -0
- package/server/codex_app_server.py +3030 -0
- package/server/codex_auth.py +93 -0
- package/server/codex_history_repair.py +1247 -0
- package/server/codex_provider.py +1120 -0
- package/server/codex_side_question.py +301 -0
- package/server/cursor_agent_client.py +654 -0
- package/server/cursor_process_guard.py +347 -0
- package/server/execution_activation.py +610 -0
- package/server/execution_control.py +233 -0
- package/server/execution_durability.py +184 -0
- package/server/execution_http.py +256 -0
- package/server/execution_install.py +792 -0
- package/server/execution_legacy_runner.py +406 -0
- package/server/execution_maintenance.py +137 -0
- package/server/execution_manage.py +615 -0
- package/server/execution_ownership.py +83 -0
- package/server/execution_preparation.py +362 -0
- package/server/execution_recovery.py +845 -0
- package/server/execution_recovery_status.py +195 -0
- package/server/execution_service.py +387 -0
- package/server/execution_transport.py +622 -0
- package/server/execution_uninstall.py +406 -0
- package/server/execution_update_status.py +49 -0
- package/server/install.sh +7074 -0
- package/server/interactive_chat_controls.py +153 -0
- package/server/interactive_chat_native.py +66 -0
- package/server/interactive_chat_projection.py +152 -0
- package/server/interactive_chat_runtime.py +132 -0
- package/server/interactive_chat_share_routes.py +534 -0
- package/server/interactive_chat_share_web.py +12 -0
- package/server/interactive_chat_shares.py +238 -0
- package/server/provider_commands.py +425 -0
- package/server/public_chat_share_routes.py +426 -0
- package/server/public_chat_shares.py +759 -0
- package/server/public_chat_transcript.py +357 -0
- package/server/pyproject.toml +29 -0
- package/server/release-public-key.pem +3 -0
- package/server/secure_peer_delivery.py +1236 -0
- package/server/secure_peer_runtime.py +7694 -0
- package/server/shared_chat_video_stream.py +262 -0
- package/server/shared_chat_videos.py +155 -0
- package/server/side_questions.py +594 -0
- package/server/team_hub_host.py +1354 -0
- package/server/team_mail_grants.py +136 -0
- package/server/team_mail_runtime.py +302 -0
- package/server/team_mail_websocket.py +211 -0
- package/server/title_generation.py +333 -0
- package/server/uninstall.sh +359 -0
- package/server/update_handoff.py +237 -0
- package/server/update_preparation.py +264 -0
- package/server/update_recovery.py +140 -0
- package/server/update_runner.py +1819 -0
- package/server/uv.lock +1101 -0
- 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
|
+
}
|