@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,245 @@
1
+ """Read native Claude goal attachments without interpreting conversation text.
2
+
3
+ Call refresh from the existing provider-history/activity path. This module owns
4
+ no watcher, timer, provider command, or transcript mutation.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from collections import deque, OrderedDict
9
+ from datetime import datetime
10
+ import json
11
+ import os
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Any
15
+ from uuid import UUID
16
+
17
+
18
+ MAX_GOAL_RECORD_BYTES = 128 * 1024
19
+ MAX_GOAL_SCAN_BYTES = 4 * 1024 * 1024
20
+
21
+
22
+ def is_claude_synthetic_no_response(event: Any) -> bool:
23
+ if not isinstance(event, dict) or event.get("type") != "assistant" or event.get("isSidechain") is True:
24
+ return False
25
+ message = event.get("message")
26
+ return bool(isinstance(message, dict) and message.get("model") == "<synthetic>"
27
+ and message.get("role") == "assistant"
28
+ and message.get("content") == [{"type": "text", "text": "No response requested."}]
29
+ and isinstance(message.get("usage"), dict)
30
+ and message["usage"].get("output_tokens") == 0)
31
+
32
+
33
+ class ClaudeGoalHistoryNormalizer:
34
+ """Normalize only commands linked to native goal attachments, never quotes."""
35
+ _command = re.compile(r"\A<command-name>/goal</command-name>\s*<command-message>goal</command-message>\s*<command-args>(.*)</command-args>\Z", re.DOTALL)
36
+
37
+ def __init__(self):
38
+ self._parents: OrderedDict[str, dict] = OrderedDict()
39
+
40
+ def seed(self, path: Path, offset: int) -> None:
41
+ if offset <= 0:
42
+ return
43
+ try:
44
+ with path.open("rb") as stream:
45
+ start = max(0, offset - 65536)
46
+ stream.seek(start)
47
+ region = stream.read(offset - start)
48
+ except OSError:
49
+ return
50
+ if start:
51
+ region = region.partition(b"\n")[2]
52
+ for line in region.splitlines():
53
+ try:
54
+ self.consume(json.loads(line))
55
+ except (ValueError, RecursionError):
56
+ continue
57
+
58
+ def consume(self, event: Any) -> Any:
59
+ if not isinstance(event, dict) or event.get("isSidechain") is True:
60
+ return event
61
+ parent = self._parents.get(event.get("parentUuid"))
62
+ same_session = bool(parent and parent.get("sessionId") == event.get("sessionId"))
63
+ message = event.get("message")
64
+ content = message.get("content") if isinstance(message, dict) else None
65
+ attachment = event.get("attachment")
66
+ if (isinstance(attachment, dict) and attachment.get("type") == "goal_status"
67
+ and attachment.get("sentinel") is True and type(attachment.get("met")) is bool):
68
+ self._remember(event)
69
+ elif (same_session and event.get("type") == "user" and event.get("isMeta") is True
70
+ and isinstance(content, str) and content.startswith("<local-command-caveat>")
71
+ and parent.get("attachment", {}).get("met") is True):
72
+ self._remember({**event, "attachment": parent["attachment"]})
73
+ elif (same_session and event.get("type") == "user" and isinstance(message, dict)
74
+ and message.get("role") == "user" and isinstance(content, str)):
75
+ match = self._command.fullmatch(content)
76
+ native = parent.get("attachment", {})
77
+ if match and ((native.get("met") is False and match[1] == native.get("condition"))
78
+ or (native.get("met") is True and match[1] == "clear"
79
+ and parent.get("isMeta") is True
80
+ and parent.get("promptId") == event.get("promptId"))):
81
+ return {**event, "message": {**message, "content": "/goal " + match[1]}}
82
+ return event
83
+
84
+ def _remember(self, event: dict) -> None:
85
+ identity = event.get("uuid")
86
+ if not isinstance(identity, str) or not isinstance(event.get("sessionId"), str):
87
+ return
88
+ native = event.get("attachment", {})
89
+ condition = native.get("condition")
90
+ if not isinstance(condition, str) or len(condition) > 16_384:
91
+ return
92
+ self._parents[identity] = {key: event.get(key) for key in ("sessionId", "promptId", "isMeta")}
93
+ self._parents[identity]["attachment"] = {
94
+ "met": native.get("met"), "condition": condition,
95
+ }
96
+ while len(self._parents) > 32:
97
+ self._parents.popitem(last=False)
98
+
99
+
100
+ def _timestamp_ms(value: Any) -> int | None:
101
+ if not isinstance(value, str) or len(value) > 64:
102
+ return None
103
+ try:
104
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
105
+ return int(parsed.timestamp() * 1000) if parsed.tzinfo else None
106
+ except (ValueError, OverflowError, OSError):
107
+ return None
108
+
109
+
110
+ def _counter(value: Any) -> int | None:
111
+ return value if type(value) is int and 0 <= value <= 2**53 - 1 else None
112
+
113
+
114
+ class ClaudeGoalProjection:
115
+ """One provider session's latest goal and bounded incremental file cursor.
116
+
117
+ A native set sentinel establishes the goal. Evaluator and clear records
118
+ update that exact condition; unrelated sessions/sidechains cannot establish
119
+ or finish it. A fork starts empty until its own native set evidence arrives.
120
+ No failure, pause, or success is inferred from a result/assistant message.
121
+ """
122
+
123
+ def __init__(self, provider_session_id: str):
124
+ self.reset(provider_session_id)
125
+
126
+ def reset(self, provider_session_id: str) -> None:
127
+ self.provider_session_id = str(provider_session_id)
128
+ self.goal: dict[str, Any] | None = None
129
+ self.offset = 0
130
+ self.caught_up = False
131
+ self._identity: tuple[str, int, int] | None = None
132
+ self._tail = b""
133
+ self._anchor = b""
134
+ self._discard_line = False
135
+ self._seen: deque[str] = deque(maxlen=256)
136
+ self._last_timestamp = -1
137
+
138
+ def consume(self, event: Any) -> bool:
139
+ """Fold one native JSONL record; return whether visible goal changed."""
140
+ if not isinstance(event, dict) or (
141
+ event.get("type") != "attachment"
142
+ or event.get("sessionId") != self.provider_session_id
143
+ or event.get("isSidechain") is not False
144
+ ):
145
+ return False
146
+ attachment = event.get("attachment")
147
+ if not isinstance(attachment, dict) or attachment.get("type") != "goal_status":
148
+ return False
149
+ event_id = event.get("uuid")
150
+ if not isinstance(event_id, str) or len(event_id) != 36:
151
+ return False
152
+ try:
153
+ UUID(event_id)
154
+ except ValueError:
155
+ return False
156
+ timestamp = _timestamp_ms(event.get("timestamp"))
157
+ condition = attachment.get("condition")
158
+ met = attachment.get("met")
159
+ sentinel = attachment.get("sentinel", False)
160
+ if (
161
+ timestamp is None or timestamp < self._last_timestamp
162
+ or event_id in self._seen
163
+ or not isinstance(condition, str) or not condition.strip()
164
+ or len(condition) > 16_384
165
+ or type(met) is not bool or type(sentinel) is not bool
166
+ ):
167
+ return False
168
+ previous = self.goal
169
+ if sentinel and not met:
170
+ goal = {"condition": condition, "status": "active", "set_at": timestamp}
171
+ else:
172
+ if previous is None or previous["condition"] != condition:
173
+ return False
174
+ if not sentinel and previous["status"] != "active":
175
+ return False
176
+ goal = dict(previous)
177
+ goal["status"] = "cleared" if sentinel else "achieved" if met else "active"
178
+ if sentinel:
179
+ for field in ("iterations", "duration_ms", "tokens"):
180
+ goal.pop(field, None)
181
+ if not sentinel:
182
+ reason = attachment.get("reason")
183
+ if isinstance(reason, str) and reason:
184
+ goal["last_reason"] = reason[:8192]
185
+ # Resuming the native session can reset its counters. The
186
+ # number of historical evaluator rows is not a native total.
187
+ for native, public in (("iterations", "iterations"), ("durationMs", "duration_ms"), ("tokens", "tokens")):
188
+ value = _counter(attachment.get(native))
189
+ if value is not None:
190
+ goal[public] = value
191
+ else:
192
+ goal.pop(public, None)
193
+ self._seen.append(event_id)
194
+ self._last_timestamp = timestamp
195
+ self.goal = goal
196
+ return goal != previous
197
+
198
+ def refresh(
199
+ self, path: str | Path, *, provider_session_id: str | None = None,
200
+ max_bytes: int = MAX_GOAL_SCAN_BYTES,
201
+ ) -> bool:
202
+ """Read at most max_bytes; later activity can continue an unfinished scan.
203
+
204
+ Partial lines are retained, oversized lines skipped, and replacement,
205
+ truncation or provider changes reset both projection and cursor. A
206
+ temporarily missing/unreadable file keeps the last observed native state.
207
+ The caller should run file I/O off the server's event loop.
208
+ """
209
+ previous = self.goal
210
+ if provider_session_id is not None and provider_session_id != self.provider_session_id:
211
+ self.reset(provider_session_id)
212
+ budget = max(1, min(int(max_bytes), MAX_GOAL_SCAN_BYTES))
213
+ try:
214
+ with Path(path).open("rb") as stream:
215
+ stat = os.fstat(stream.fileno())
216
+ identity = (str(Path(path).absolute()), stat.st_dev, stat.st_ino)
217
+ stream.seek(max(0, self.offset - len(self._anchor)))
218
+ anchor_matches = stream.read(len(self._anchor)) == self._anchor
219
+ if self._identity != identity or stat.st_size < self.offset or not anchor_matches:
220
+ self.reset(self.provider_session_id)
221
+ self._identity = identity
222
+ stream.seek(self.offset)
223
+ chunk = stream.read(budget)
224
+ self.offset += len(chunk)
225
+ self._anchor = (self._anchor + chunk)[-64:]
226
+ self.caught_up = self.offset >= stat.st_size
227
+ except OSError:
228
+ self.caught_up = False
229
+ return previous != self.goal
230
+ lines = (self._tail + chunk).split(b"\n")
231
+ self._tail = lines.pop()
232
+ for line in lines:
233
+ if self._discard_line:
234
+ self._discard_line = False
235
+ continue
236
+ if len(line) > MAX_GOAL_RECORD_BYTES or b'"goal_status"' not in line:
237
+ continue
238
+ try:
239
+ self.consume(json.loads(line))
240
+ except (ValueError, RecursionError):
241
+ continue
242
+ if len(self._tail) > MAX_GOAL_RECORD_BYTES:
243
+ self._tail = b""
244
+ self._discard_line = True
245
+ return previous != self.goal
@@ -0,0 +1,137 @@
1
+ """Pure, bounded Claude source-lineage classification. Never edits transcripts.
2
+
3
+ The marker's wording is not evidence of a human Stop action. A synthetic user
4
+ record must belong to an already established prompt, not begin a new prompt.
5
+ Only source identifiers are carried between byte-cursor reads; never chat text.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ import re
11
+
12
+
13
+ INTERRUPTION_MARKERS = frozenset({
14
+ "[Request interrupted by user]",
15
+ "[Request interrupted by user for tool use]",
16
+ })
17
+ _UUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\Z")
18
+ _TIME = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)\Z")
19
+ _MAX_BRANCH_EVENTS = 128
20
+
21
+
22
+ def _uuid(value) -> str | None:
23
+ return value if isinstance(value, str) and _UUID.fullmatch(value) else None
24
+
25
+
26
+ def _timestamp(value) -> str | None:
27
+ if not isinstance(value, str) or not _TIME.fullmatch(value):
28
+ return None
29
+ try:
30
+ return value if datetime.fromisoformat(value.replace("Z", "+00:00")).tzinfo else None
31
+ except ValueError:
32
+ return None
33
+
34
+
35
+ def normalize_claude_interruption_context(value, provider_session_id=None) -> dict:
36
+ """Allowlist a tiny initialized cursor state, including conservative empties."""
37
+ empty = {"version": 1}
38
+ if not isinstance(value, dict) or value.get("version") != 1:
39
+ return empty
40
+ keys = ("session_id", "prompt_id", "anchor_event_id", "last_event_id")
41
+ if any(not _uuid(value.get(key)) for key in keys):
42
+ return empty
43
+ if provider_session_id and value["session_id"] != provider_session_id:
44
+ return empty
45
+ context = {"version": 1, **{key: value[key] for key in keys}}
46
+ branch_ids = value.get("branch_event_ids")
47
+ if branch_ids is not None:
48
+ if (not isinstance(branch_ids, list) or not 1 <= len(branch_ids) <= _MAX_BRANCH_EVENTS
49
+ or any(not _uuid(item) for item in branch_ids)
50
+ or context["last_event_id"] not in branch_ids):
51
+ return empty
52
+ context["branch_event_ids"] = list(dict.fromkeys(branch_ids))
53
+ return context
54
+
55
+
56
+ class ClaudeInterruptionTracker:
57
+ def __init__(self, initial_context=None):
58
+ self._context = normalize_claude_interruption_context(initial_context)
59
+
60
+ def export_context(self) -> dict:
61
+ return normalize_claude_interruption_context(self._context)
62
+
63
+ def consume(self, event) -> dict | None:
64
+ if not isinstance(event, dict):
65
+ self._context = {"version": 1}
66
+ return None
67
+ if event.get("isSidechain") is True:
68
+ return None
69
+ event_id = _uuid(event.get("uuid"))
70
+ # Non-message queue bookkeeping has no UUID and is outside the lineage.
71
+ if event_id is None:
72
+ if not isinstance(event.get("type"), str) or event.get("type") in ("user", "assistant", "attachment"):
73
+ self._context = {"version": 1}
74
+ return None
75
+ session_id = _uuid(event.get("sessionId"))
76
+ parent_id = _uuid(event.get("parentUuid"))
77
+ prompt_id = _uuid(event.get("promptId"))
78
+ timestamp = _timestamp(event.get("timestamp"))
79
+ message = event.get("message")
80
+ content = message.get("content") if isinstance(message, dict) else None
81
+ blocks = content if isinstance(content, list) else []
82
+ marker = (
83
+ event.get("type") == "user"
84
+ and isinstance(message, dict) and message.get("role") == "user"
85
+ and len(blocks) == 1 and isinstance(blocks[0], dict)
86
+ and blocks[0].get("type") == "text"
87
+ and isinstance(blocks[0].get("text"), str)
88
+ and blocks[0]["text"] in INTERRUPTION_MARKERS
89
+ )
90
+ context = self._context
91
+ # Parallel tool results can be siblings of the same assistant record.
92
+ # Keep their proven parents across incremental cursor reads; immediate
93
+ # adjacency alone is not Claude's transcript ancestry contract.
94
+ branch_ids = context.get("branch_event_ids", [context.get("last_event_id")])
95
+ continued = (
96
+ session_id is not None and session_id == context.get("session_id")
97
+ and parent_id is not None and parent_id in branch_ids
98
+ and (prompt_id is None or prompt_id == context.get("prompt_id"))
99
+ )
100
+ origin = None
101
+ if marker and session_id and timestamp and (
102
+ event.get("isMeta") is True
103
+ or (continued and prompt_id is not None and prompt_id == context.get("prompt_id")
104
+ and event_id != context.get("anchor_event_id")
105
+ and parent_id != context.get("anchor_event_id"))
106
+ ):
107
+ origin = {
108
+ "provider": "claude", "kind": "interruption", "cause": "unknown",
109
+ "event_id": event_id, "session_id": session_id, "timestamp": timestamp,
110
+ }
111
+ if parent_id:
112
+ origin["parent_event_id"] = parent_id
113
+ if prompt_id:
114
+ origin["prompt_id"] = prompt_id
115
+
116
+ has_text = isinstance(content, str) and bool(content.strip()) or any(
117
+ isinstance(block, dict) and block.get("type") == "text"
118
+ and isinstance(block.get("text"), str) and bool(block["text"].strip())
119
+ for block in blocks
120
+ )
121
+ tool_result = any(isinstance(block, dict) and block.get("type") == "tool_result" for block in blocks)
122
+ real_prompt = (
123
+ event.get("type") == "user" and isinstance(message, dict)
124
+ and message.get("role") == "user" and has_text and not tool_result
125
+ and not marker and event.get("isMeta") is not True
126
+ and session_id and prompt_id and timestamp
127
+ )
128
+ if real_prompt:
129
+ self._context = {"version": 1, "session_id": session_id, "prompt_id": prompt_id,
130
+ "anchor_event_id": event_id, "last_event_id": event_id,
131
+ "branch_event_ids": [event_id]}
132
+ elif continued:
133
+ self._context = {**context, "last_event_id": event_id,
134
+ "branch_event_ids": [*branch_ids, event_id][-_MAX_BRANCH_EVENTS:]}
135
+ else:
136
+ self._context = {"version": 1}
137
+ return origin