ccx-native 0.0.1 → 0.1.0
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 +21 -0
- package/README.md +241 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Reconstruct a native thread from source-attested paged Codex history."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
_PAGE_SIZE = 100
|
|
8
|
+
_MAX_PAGES = 10_000
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _remaining(deadline: float) -> float:
|
|
12
|
+
remaining = deadline - time.monotonic()
|
|
13
|
+
if remaining <= 0:
|
|
14
|
+
raise TimeoutError("Codex history paging timed out")
|
|
15
|
+
return remaining
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _full_turn_items(request: Callable[..., dict], thread_id: str,
|
|
19
|
+
turn_id: str, deadline: float) -> list[dict]:
|
|
20
|
+
items: list[dict] = []
|
|
21
|
+
cursor: str | None = None
|
|
22
|
+
seen: set[str] = set()
|
|
23
|
+
for _ in range(_MAX_PAGES):
|
|
24
|
+
params: dict[str, Any] = {
|
|
25
|
+
"threadId": thread_id, "turnId": turn_id,
|
|
26
|
+
"limit": _PAGE_SIZE, "sortDirection": "asc",
|
|
27
|
+
}
|
|
28
|
+
if cursor is not None:
|
|
29
|
+
params["cursor"] = cursor
|
|
30
|
+
page = request("thread/items/list", params, timeout=_remaining(deadline))
|
|
31
|
+
entries = page.get("data") if isinstance(page, dict) else None
|
|
32
|
+
if not isinstance(entries, list):
|
|
33
|
+
raise ValueError("Codex item-history page is malformed")
|
|
34
|
+
for wrapper in entries:
|
|
35
|
+
if (not isinstance(wrapper, dict) or wrapper.get("turnId") != turn_id
|
|
36
|
+
or not isinstance(wrapper.get("item"), dict)):
|
|
37
|
+
raise ValueError("Codex item-history page belongs to another turn")
|
|
38
|
+
items.append(wrapper["item"])
|
|
39
|
+
next_cursor = page.get("nextCursor")
|
|
40
|
+
if next_cursor is None:
|
|
41
|
+
return items
|
|
42
|
+
if (not isinstance(next_cursor, str) or not next_cursor
|
|
43
|
+
or next_cursor in seen or not entries):
|
|
44
|
+
raise ValueError("Codex item-history cursor did not advance")
|
|
45
|
+
seen.add(next_cursor)
|
|
46
|
+
cursor = next_cursor
|
|
47
|
+
raise ValueError("Codex item history exceeded page budget")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fill_thread(
|
|
51
|
+
thread: dict, request: Callable[..., dict], *, deadline: float,
|
|
52
|
+
limit: int | None = None, newest_first: bool = False,
|
|
53
|
+
exact_turn_id: str | None = None,
|
|
54
|
+
) -> dict:
|
|
55
|
+
"""Return the thread/read shape, with actual turn statuses and full items."""
|
|
56
|
+
thread_id = thread.get("id") if isinstance(thread, dict) else None
|
|
57
|
+
if not isinstance(thread_id, str) or not thread_id:
|
|
58
|
+
raise ValueError("Codex source metadata has no original thread id")
|
|
59
|
+
if limit is not None and (type(limit) is not int or limit < 1):
|
|
60
|
+
raise ValueError("Codex history turn limit must be positive")
|
|
61
|
+
if exact_turn_id is not None and (not isinstance(exact_turn_id, str)
|
|
62
|
+
or not exact_turn_id):
|
|
63
|
+
raise ValueError("Codex exact turn id is invalid")
|
|
64
|
+
descending = newest_first or exact_turn_id is not None
|
|
65
|
+
turns: list[dict] = []
|
|
66
|
+
seen: set[str] = set()
|
|
67
|
+
seen_turns: set[str] = set()
|
|
68
|
+
cursor: str | None = None
|
|
69
|
+
for _ in range(_MAX_PAGES):
|
|
70
|
+
params: dict[str, Any] = {
|
|
71
|
+
"threadId": thread_id,
|
|
72
|
+
"limit": min(_PAGE_SIZE, limit - len(turns)) if limit else _PAGE_SIZE,
|
|
73
|
+
"sortDirection": "desc" if descending else "asc",
|
|
74
|
+
"itemsView": "notLoaded" if exact_turn_id else "full",
|
|
75
|
+
}
|
|
76
|
+
if cursor is not None:
|
|
77
|
+
params["cursor"] = cursor
|
|
78
|
+
page = request("thread/turns/list", params, timeout=_remaining(deadline))
|
|
79
|
+
data = page.get("data") if isinstance(page, dict) else None
|
|
80
|
+
if not isinstance(data, list):
|
|
81
|
+
raise ValueError("Codex turn-history page is malformed")
|
|
82
|
+
for turn in data:
|
|
83
|
+
if (not isinstance(turn, dict) or not isinstance(turn.get("id"), str)
|
|
84
|
+
or not turn["id"] or not isinstance(turn.get("status"), str)
|
|
85
|
+
or not isinstance(turn.get("items"), list)
|
|
86
|
+
or turn["id"] in seen_turns):
|
|
87
|
+
raise ValueError("Codex native turn-history entry is malformed")
|
|
88
|
+
seen_turns.add(turn["id"])
|
|
89
|
+
if exact_turn_id is not None and turn["id"] != exact_turn_id:
|
|
90
|
+
continue
|
|
91
|
+
result = dict(turn)
|
|
92
|
+
if turn.get("itemsView", "full") != "full":
|
|
93
|
+
result["items"] = _full_turn_items(request, thread_id, turn["id"], deadline)
|
|
94
|
+
result["itemsView"] = "full"
|
|
95
|
+
turns.append(result)
|
|
96
|
+
if exact_turn_id is not None or (limit is not None and len(turns) >= limit):
|
|
97
|
+
return {**thread, "turns": list(reversed(turns)) if descending else turns,
|
|
98
|
+
"historySource": "thread/turns/list"}
|
|
99
|
+
next_cursor = page.get("nextCursor")
|
|
100
|
+
if next_cursor is None:
|
|
101
|
+
return {**thread, "turns": list(reversed(turns)) if descending else turns,
|
|
102
|
+
"historySource": "thread/turns/list"}
|
|
103
|
+
if (not isinstance(next_cursor, str) or not next_cursor
|
|
104
|
+
or next_cursor in seen or not data):
|
|
105
|
+
raise ValueError("Codex turn-history cursor did not advance")
|
|
106
|
+
seen.add(next_cursor)
|
|
107
|
+
cursor = next_cursor
|
|
108
|
+
raise ValueError("Codex turn history exceeded page budget")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def read_original_thread(request: Callable[..., dict], thread_id: str, *,
|
|
112
|
+
deadline: float, limit: int | None = None,
|
|
113
|
+
newest_first: bool = False,
|
|
114
|
+
exact_turn_id: str | None = None,
|
|
115
|
+
expected_source: str | None = None) -> dict:
|
|
116
|
+
"""One provider connection: metadata proof followed by bounded page reads."""
|
|
117
|
+
reply = request("thread/read", {"threadId": thread_id, "includeTurns": False},
|
|
118
|
+
timeout=_remaining(deadline))
|
|
119
|
+
thread = reply.get("thread") if isinstance(reply, dict) else None
|
|
120
|
+
if not isinstance(thread, dict) or thread.get("id") != thread_id:
|
|
121
|
+
raise ValueError("Codex source metadata did not return the expected thread")
|
|
122
|
+
if expected_source is not None and thread.get("threadSource") != expected_source:
|
|
123
|
+
raise ValueError("Codex original thread source changed before history paging")
|
|
124
|
+
return fill_thread(thread, request, deadline=deadline, limit=limit,
|
|
125
|
+
newest_first=newest_first, exact_turn_id=exact_turn_id)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def read_fork_source_point(request: Callable[..., dict], thread_id: str, *,
|
|
129
|
+
deadline: float) -> dict:
|
|
130
|
+
"""Read only an unmanaged native source's metadata and newest turn.
|
|
131
|
+
|
|
132
|
+
This is NOT worker history authorization. It never starts, resumes,
|
|
133
|
+
unarchives or mutates the native source. Admission is a separate child
|
|
134
|
+
thread/fork with its own independently verified source tag.
|
|
135
|
+
"""
|
|
136
|
+
from uuid import UUID
|
|
137
|
+
if (not isinstance(thread_id, str)
|
|
138
|
+
or str(UUID(thread_id)) != thread_id):
|
|
139
|
+
raise ValueError("import needs one exact native Codex thread UUID")
|
|
140
|
+
reply = request(
|
|
141
|
+
"thread/read", {"threadId": thread_id, "includeTurns": False},
|
|
142
|
+
timeout=_remaining(deadline),
|
|
143
|
+
)
|
|
144
|
+
thread = reply.get("thread") if isinstance(reply, dict) else None
|
|
145
|
+
if not isinstance(thread, dict) or thread.get("id") != thread_id:
|
|
146
|
+
raise ValueError("Codex native import source ID is not proven")
|
|
147
|
+
page = request("thread/turns/list", {
|
|
148
|
+
"threadId": thread_id, "limit": 1,
|
|
149
|
+
"sortDirection": "desc", "itemsView": "notLoaded",
|
|
150
|
+
}, timeout=_remaining(deadline))
|
|
151
|
+
items = page.get("data") if isinstance(page, dict) else None
|
|
152
|
+
if not isinstance(items, list) or len(items) > 1:
|
|
153
|
+
raise ValueError("Codex native import source turn page malformed")
|
|
154
|
+
last = items[0] if items else None
|
|
155
|
+
if last is not None and (not isinstance(last, dict)
|
|
156
|
+
or not isinstance(last.get("id"), str)):
|
|
157
|
+
raise ValueError("Codex native import source turn ID unavailable")
|
|
158
|
+
return {"thread": thread, "lastTurn": last}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Targeted Codex parent-notification delivery reconciliation.
|
|
2
|
+
|
|
3
|
+
Notification delivery only needs to answer one question: did a native
|
|
4
|
+
``worker_notification`` tool output with this delivery id already land?
|
|
5
|
+
Hydrating an entire thread with ``thread/read(includeTurns=true)`` is both
|
|
6
|
+
unnecessary and harmful for long-lived paginated Codex threads. Modern Codex
|
|
7
|
+
exposes ``thread/items/list`` for incremental history access, so CCX pages
|
|
8
|
+
that newest-first and never hydrates the full history for reconciliation.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
ITEM_PAGE_SIZE = 100
|
|
17
|
+
MAX_ITEM_PAGES = 1000
|
|
18
|
+
ITEM_PAGE_TIMEOUT_S = 10.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _notification_payload(item: Any, *, name: str = "worker_notification") -> dict[str, Any] | None:
|
|
22
|
+
if not isinstance(item, dict):
|
|
23
|
+
return None
|
|
24
|
+
# ``thread/items/list`` wraps each native item with its containing turn id.
|
|
25
|
+
candidate = item.get("item") if isinstance(item.get("item"), dict) else item
|
|
26
|
+
if not isinstance(candidate, dict):
|
|
27
|
+
return None
|
|
28
|
+
if (
|
|
29
|
+
candidate.get("type") != "functionCallOutput"
|
|
30
|
+
or candidate.get("name") != name
|
|
31
|
+
):
|
|
32
|
+
return None
|
|
33
|
+
raw = candidate.get("output")
|
|
34
|
+
if isinstance(raw, dict):
|
|
35
|
+
return raw
|
|
36
|
+
if not isinstance(raw, str):
|
|
37
|
+
return None
|
|
38
|
+
try:
|
|
39
|
+
value = json.loads(raw)
|
|
40
|
+
except json.JSONDecodeError:
|
|
41
|
+
return None
|
|
42
|
+
return value if isinstance(value, dict) else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _item_delivery_match(
|
|
46
|
+
item: Any,
|
|
47
|
+
delivery_id: str,
|
|
48
|
+
*,
|
|
49
|
+
legacy_event_id: str | None = None,
|
|
50
|
+
name: str = "worker_notification",
|
|
51
|
+
) -> dict[str, Any] | None:
|
|
52
|
+
value = _notification_payload(item, name=name)
|
|
53
|
+
if value is None:
|
|
54
|
+
return None
|
|
55
|
+
matches = (
|
|
56
|
+
value.get("deliveryId") == delivery_id
|
|
57
|
+
or bool(
|
|
58
|
+
legacy_event_id
|
|
59
|
+
and value.get("deliveryId") is None
|
|
60
|
+
and value.get("eventId") == legacy_event_id
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
if not matches:
|
|
64
|
+
return None
|
|
65
|
+
turn_id = item.get("turnId") if isinstance(item, dict) else None
|
|
66
|
+
return {
|
|
67
|
+
"deliveryId": delivery_id,
|
|
68
|
+
"eventId": value.get("eventId"),
|
|
69
|
+
"turnId": turn_id if isinstance(turn_id, str) and turn_id else None,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _paged_delivery_match(
|
|
74
|
+
request: Callable[..., dict[str, Any]],
|
|
75
|
+
thread_id: str,
|
|
76
|
+
delivery_id: str,
|
|
77
|
+
*,
|
|
78
|
+
legacy_event_id: str | None = None,
|
|
79
|
+
stop_at_turn_id: str | None = None,
|
|
80
|
+
name: str = "worker_notification",
|
|
81
|
+
) -> dict[str, Any] | None:
|
|
82
|
+
cursor: str | None = None
|
|
83
|
+
seen_cursors: set[str] = set()
|
|
84
|
+
for _ in range(MAX_ITEM_PAGES):
|
|
85
|
+
params: dict[str, Any] = {
|
|
86
|
+
"threadId": thread_id,
|
|
87
|
+
"limit": ITEM_PAGE_SIZE,
|
|
88
|
+
"sortDirection": "desc",
|
|
89
|
+
}
|
|
90
|
+
if cursor is not None:
|
|
91
|
+
params["cursor"] = cursor
|
|
92
|
+
result = request("thread/items/list", params, timeout=ITEM_PAGE_TIMEOUT_S)
|
|
93
|
+
data = result.get("data") if isinstance(result, dict) else None
|
|
94
|
+
if not isinstance(data, list):
|
|
95
|
+
raise RuntimeError("thread/items/list returned malformed data")
|
|
96
|
+
for entry in data:
|
|
97
|
+
match = _item_delivery_match(
|
|
98
|
+
entry, delivery_id, legacy_event_id=legacy_event_id, name=name
|
|
99
|
+
)
|
|
100
|
+
if match is not None:
|
|
101
|
+
return match
|
|
102
|
+
if (
|
|
103
|
+
stop_at_turn_id
|
|
104
|
+
and isinstance(entry, dict)
|
|
105
|
+
and entry.get("turnId") == stop_at_turn_id
|
|
106
|
+
):
|
|
107
|
+
# Newest-first reached a turn that had already completed
|
|
108
|
+
# before the first attempt; nothing older can hold it.
|
|
109
|
+
return None
|
|
110
|
+
next_cursor = result.get("nextCursor")
|
|
111
|
+
if not isinstance(next_cursor, str) or not next_cursor:
|
|
112
|
+
return None
|
|
113
|
+
if next_cursor in seen_cursors:
|
|
114
|
+
raise RuntimeError("thread/items/list repeated a pagination cursor")
|
|
115
|
+
seen_cursors.add(next_cursor)
|
|
116
|
+
cursor = next_cursor
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
f"thread/items/list exceeded {MAX_ITEM_PAGES} pages while reconciling notification"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def notification_delivery_match(
|
|
123
|
+
request: Callable[..., dict[str, Any]],
|
|
124
|
+
thread_id: str,
|
|
125
|
+
delivery_id: str,
|
|
126
|
+
*,
|
|
127
|
+
legacy_event_id: str | None = None,
|
|
128
|
+
stop_at_turn_id: str | None = None,
|
|
129
|
+
name: str = "worker_notification",
|
|
130
|
+
) -> dict[str, Any] | None:
|
|
131
|
+
"""Find a landed notification, newest first.
|
|
132
|
+
|
|
133
|
+
``stop_at_turn_id`` names a turn that had completed before the delivery's
|
|
134
|
+
first attempt: the paged walk ends there instead of at the thread's start.
|
|
135
|
+
"""
|
|
136
|
+
return _paged_delivery_match(
|
|
137
|
+
request, thread_id, delivery_id,
|
|
138
|
+
legacy_event_id=legacy_event_id, stop_at_turn_id=stop_at_turn_id,
|
|
139
|
+
name=name,
|
|
140
|
+
)
|