ccx-native 0.0.0-stage → 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 -2
- package/bin/ccx +56 -0
- 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 +40 -4
- 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,397 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
from ccx.store.liveness import recorded_host_loaded
|
|
8
|
+
from ccx.client import CcxClient
|
|
9
|
+
from ccx.codex.client import CodexClient
|
|
10
|
+
from ccx.errors import (
|
|
11
|
+
InvalidRequest,
|
|
12
|
+
SessionUnavailable,
|
|
13
|
+
worker_not_found,
|
|
14
|
+
)
|
|
15
|
+
from ccx.workers import WorkerClient
|
|
16
|
+
from ccx.store.identity import canonical_worker_id
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_TRANSCRIPT_LOCATOR_PREFIX = "ccxt2."
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_TRANSCRIPT_CURSOR_PREFIX = "ccxtc2."
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_DEFAULT_TRANSCRIPT_PAGE = 20
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_DEFAULT_TRANSCRIPT_TOOL_BYTES = 64 * 1024
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_DEFAULT_REPORT_CHARS = 64 * 1024
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
MAX_CALLER_METADATA_BYTES = 4 * 1024
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_ENGINES = frozenset({"claude", "codex"})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_PERMISSIONS = frozenset({"full", "restricted"})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _LazyClient:
|
|
44
|
+
"""Keep observational protocol imports free of directory initialization."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, factory):
|
|
47
|
+
self.factory = factory
|
|
48
|
+
self.value = None
|
|
49
|
+
|
|
50
|
+
def __getattr__(self, name):
|
|
51
|
+
if self.value is None:
|
|
52
|
+
self.value = self.factory()
|
|
53
|
+
return getattr(self.value, name)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_CLIENT = _LazyClient(CcxClient)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_CODEX_CLIENT = _LazyClient(CodexClient)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_WORKERS = _LazyClient(WorkerClient)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _normalize_caller_metadata(
|
|
66
|
+
value: dict[str, Any] | None,
|
|
67
|
+
*,
|
|
68
|
+
notify_parent: bool,
|
|
69
|
+
) -> dict[str, Any] | None:
|
|
70
|
+
if value is None:
|
|
71
|
+
return None
|
|
72
|
+
if not notify_parent:
|
|
73
|
+
raise InvalidRequest(
|
|
74
|
+
"send: caller_metadata requires notify_parent=true"
|
|
75
|
+
)
|
|
76
|
+
if not isinstance(value, dict):
|
|
77
|
+
raise InvalidRequest("send: caller_metadata must be a JSON object")
|
|
78
|
+
try:
|
|
79
|
+
encoded = json.dumps(
|
|
80
|
+
value,
|
|
81
|
+
sort_keys=True,
|
|
82
|
+
separators=(",", ":"),
|
|
83
|
+
ensure_ascii=False,
|
|
84
|
+
allow_nan=False,
|
|
85
|
+
).encode("utf-8")
|
|
86
|
+
except (TypeError, ValueError, OverflowError) as error:
|
|
87
|
+
raise InvalidRequest(
|
|
88
|
+
"send: caller_metadata must contain only valid JSON values"
|
|
89
|
+
) from error
|
|
90
|
+
if len(encoded) > MAX_CALLER_METADATA_BYTES:
|
|
91
|
+
raise InvalidRequest(
|
|
92
|
+
"send: caller_metadata exceeds the 4096-byte serialized limit"
|
|
93
|
+
)
|
|
94
|
+
# Return a detached, JSON-normalized object so later caller mutation cannot
|
|
95
|
+
# change the durable semantic value between receipt and notification writes.
|
|
96
|
+
return json.loads(encoded.decode("utf-8"))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def split_target(target: str) -> tuple[str, str, str]:
|
|
100
|
+
"""Exact globally owned full UUID; never short/name/provider aliases.
|
|
101
|
+
|
|
102
|
+
Direct protocol helper now requires the same sealed ACTIVE generation as
|
|
103
|
+
CLI, one-shot and direct Python. It returns (full handle, engine, full ID),
|
|
104
|
+
not the previous short. No native discovery or legacy record fallback.
|
|
105
|
+
"""
|
|
106
|
+
from ccx.constants import CCX_ROOT
|
|
107
|
+
from ccx.store.root import GenerationError, require_ccx_active
|
|
108
|
+
from ccx.store.identity import UnknownWorker, IdentityError
|
|
109
|
+
try:
|
|
110
|
+
store = require_ccx_active(CCX_ROOT)
|
|
111
|
+
owner = store.resolve(target)
|
|
112
|
+
return owner.worker, owner.engine, owner.worker_id
|
|
113
|
+
except UnknownWorker as exc:
|
|
114
|
+
raise worker_not_found(target) from exc
|
|
115
|
+
except IdentityError as exc:
|
|
116
|
+
raise InvalidRequest(f"target must be a canonical managed full UUID: {exc}") from exc
|
|
117
|
+
except (GenerationError, OSError) as exc:
|
|
118
|
+
raise SessionUnavailable(
|
|
119
|
+
f"CCX worker records are unavailable; check CCX_ROOT: {exc}"
|
|
120
|
+
) from exc
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _encode_opaque(prefix: str, payload: dict[str, Any]) -> str:
|
|
124
|
+
raw = json.dumps(
|
|
125
|
+
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
126
|
+
).encode("utf-8")
|
|
127
|
+
return prefix + base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _decode_opaque(prefix: str, value: str, *, label: str) -> dict[str, Any]:
|
|
131
|
+
if not isinstance(value, str) or not value.startswith(prefix):
|
|
132
|
+
raise InvalidRequest(f"invalid {label}")
|
|
133
|
+
body = value[len(prefix):]
|
|
134
|
+
padding = "=" * ((4 - len(body) % 4) % 4)
|
|
135
|
+
try:
|
|
136
|
+
decoded = base64.urlsafe_b64decode((body + padding).encode("ascii"))
|
|
137
|
+
payload = json.loads(decoded)
|
|
138
|
+
except (ValueError, UnicodeError, TypeError) as error:
|
|
139
|
+
raise InvalidRequest(f"invalid {label}") from error
|
|
140
|
+
if not isinstance(payload, dict):
|
|
141
|
+
raise InvalidRequest(f"invalid {label}")
|
|
142
|
+
return payload
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _transcript_locator_owned(store, owner, rec: dict[str, Any]) -> str | None:
|
|
146
|
+
"""Total, I/O-free token projection of an already-loaded original binding.
|
|
147
|
+
|
|
148
|
+
Inspect is also embedded in *completed* native-effect receipts. An absent
|
|
149
|
+
or contradictory binding must never turn such a receipt into a failure.
|
|
150
|
+
The original source is independently re-proven by conversation() on read.
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
if (not isinstance(rec, dict) or rec.get("workerId") != owner.worker_id
|
|
154
|
+
or rec.get("engine") != owner.engine
|
|
155
|
+
or rec.get("fingerprintVersion") != 2
|
|
156
|
+
or not isinstance(rec.get("createSha256"), str)
|
|
157
|
+
or len(rec["createSha256"]) != 64
|
|
158
|
+
or any(ch not in "0123456789abcdef" for ch in rec["createSha256"])
|
|
159
|
+
or rec.get("terminalOnly")):
|
|
160
|
+
return None
|
|
161
|
+
canonical_worker_id(owner.worker_id)
|
|
162
|
+
native = rec.get("native")
|
|
163
|
+
if not isinstance(native, dict):
|
|
164
|
+
return None
|
|
165
|
+
if owner.engine == "claude":
|
|
166
|
+
canonical_worker_id(native.get("sessionId"))
|
|
167
|
+
if (native.get("nativeState") != "session-bound"
|
|
168
|
+
or not isinstance(rec.get("transcript"), str)
|
|
169
|
+
or not rec["transcript"]):
|
|
170
|
+
return None
|
|
171
|
+
elif owner.engine == "codex":
|
|
172
|
+
if (native.get("nativeState") != "thread-bound"
|
|
173
|
+
or not isinstance(native.get("threadId"), str)
|
|
174
|
+
or not native["threadId"]
|
|
175
|
+
or native.get("threadSource") != store.codex_source(owner.worker)):
|
|
176
|
+
return None
|
|
177
|
+
else:
|
|
178
|
+
return None
|
|
179
|
+
return _encode_opaque(_TRANSCRIPT_LOCATOR_PREFIX, {
|
|
180
|
+
"v": 2, "engine": owner.engine, "worker_id": owner.worker_id,
|
|
181
|
+
"root": hashlib.sha256(os.fsencode(store.root)).hexdigest(),
|
|
182
|
+
"generation": store.generation,
|
|
183
|
+
})
|
|
184
|
+
except Exception:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def transcript_locator(target: str) -> str | None:
|
|
189
|
+
"""Locator for one exact owned full UUID; no legacy short/name aliases."""
|
|
190
|
+
from ccx.constants import CCX_ROOT
|
|
191
|
+
from ccx.store.root import GenerationError, require_ccx_active
|
|
192
|
+
from ccx.store.identity import IdentityError, UnknownWorker
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
store = require_ccx_active(CCX_ROOT)
|
|
196
|
+
owner = store.resolve(target)
|
|
197
|
+
rec = store.record(owner.worker)
|
|
198
|
+
locator = _transcript_locator_owned(store, owner, rec)
|
|
199
|
+
store.revalidate()
|
|
200
|
+
return locator
|
|
201
|
+
except UnknownWorker as error:
|
|
202
|
+
raise worker_not_found(target) from error
|
|
203
|
+
except IdentityError as error:
|
|
204
|
+
raise InvalidRequest(f"Invalid worker for conversation read; use a worker handle: {error}") from error
|
|
205
|
+
except (GenerationError, OSError) as error:
|
|
206
|
+
raise SessionUnavailable(f"Cannot locate this worker conversation; check the worker: {error}") from error
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def recorded_inspector(store, target: str) -> dict[str, Any]:
|
|
210
|
+
from ccx.ops.forking import public_lineage
|
|
211
|
+
|
|
212
|
+
owner = store.resolve(target)
|
|
213
|
+
rec = store.record(owner.worker)
|
|
214
|
+
snapshot = rec.get("backgroundTaskSummary")
|
|
215
|
+
if isinstance(snapshot, dict):
|
|
216
|
+
background = background_task_block(
|
|
217
|
+
recorded_background_facts(snapshot), supported=True)
|
|
218
|
+
else:
|
|
219
|
+
# Historical Claude inspection can still use the explicit diagnostic
|
|
220
|
+
# extractor; the normal recorded owner snapshot is preferred.
|
|
221
|
+
facts = (CcxClient._uuid_background_task_facts_owned(store, owner, rec)
|
|
222
|
+
if owner.engine == "claude" else None)
|
|
223
|
+
background = background_task_block(facts, supported=owner.engine == "claude")
|
|
224
|
+
lifecycle = rec.get("lifecycle")
|
|
225
|
+
lifecycle = lifecycle if isinstance(lifecycle, dict) else {}
|
|
226
|
+
native = rec.get("native")
|
|
227
|
+
native = native if isinstance(native, dict) else {}
|
|
228
|
+
args = rec.get("createArgs")
|
|
229
|
+
args = args if isinstance(args, dict) else {}
|
|
230
|
+
stats = rec.get("stats")
|
|
231
|
+
stats = stats if isinstance(stats, dict) else {}
|
|
232
|
+
return {
|
|
233
|
+
"worker": owner.worker,
|
|
234
|
+
"worker_id": owner.worker_id,
|
|
235
|
+
"engine": owner.engine,
|
|
236
|
+
"short": owner.short, # display only
|
|
237
|
+
"name": rec.get("name"),
|
|
238
|
+
"cwd": rec.get("cwd"),
|
|
239
|
+
"forked_from": public_lineage(rec),
|
|
240
|
+
"state": {
|
|
241
|
+
"state": lifecycle.get("state"),
|
|
242
|
+
"native_state": native.get("nativeState"),
|
|
243
|
+
"source": "recorded",
|
|
244
|
+
},
|
|
245
|
+
"host": "unobserved",
|
|
246
|
+
"loaded": recorded_host_loaded(owner.engine, lifecycle),
|
|
247
|
+
"native": {
|
|
248
|
+
"session_id": native.get("sessionId") if owner.engine == "claude" else None,
|
|
249
|
+
"thread_id": native.get("threadId") if owner.engine == "codex" else None,
|
|
250
|
+
"thread_source": native.get("threadSource") if owner.engine == "codex" else None,
|
|
251
|
+
},
|
|
252
|
+
"pending_input": None,
|
|
253
|
+
"pending_input_observed": False,
|
|
254
|
+
"created_at": rec.get("createdAt"),
|
|
255
|
+
"last_turn_at": rec.get("lastTurnAt"),
|
|
256
|
+
"managed_turns": stats.get("turns"),
|
|
257
|
+
"model": args.get("model"),
|
|
258
|
+
"effort": args.get("effort"),
|
|
259
|
+
"terminal_only": bool(rec.get("terminalOnly")),
|
|
260
|
+
"record_revision": rec.get("recordRevision"),
|
|
261
|
+
"context": None,
|
|
262
|
+
"transcript_locator": _transcript_locator_owned(store, owner, rec),
|
|
263
|
+
"active_turn_ref": None,
|
|
264
|
+
"active_turn_ref_source": "unobserved",
|
|
265
|
+
**background,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _v1_postnative_inspect(
|
|
270
|
+
info: dict, expected_generation: str | None, *, unproven: str, incomplete: str,
|
|
271
|
+
) -> dict[str, Any]:
|
|
272
|
+
from ccx import constants
|
|
273
|
+
from ccx.errors import CreatePartialFailure
|
|
274
|
+
from ccx.store.root import require_ccx_active
|
|
275
|
+
|
|
276
|
+
if (info.get("native_ready") is not True
|
|
277
|
+
or info.get("activation_state") != "confirmed"):
|
|
278
|
+
raise CreatePartialFailure(
|
|
279
|
+
unproven, info=info, phase="native-activation",
|
|
280
|
+
)
|
|
281
|
+
try:
|
|
282
|
+
store = require_ccx_active(
|
|
283
|
+
constants.CCX_ROOT, expected_generation=expected_generation,
|
|
284
|
+
)
|
|
285
|
+
inspected = recorded_inspector(store, info["worker"])
|
|
286
|
+
store.revalidate()
|
|
287
|
+
return {
|
|
288
|
+
**inspected, "worker_id": info["worker_id"],
|
|
289
|
+
"created": info["created"], "reconciled": info["reconciled"],
|
|
290
|
+
}
|
|
291
|
+
except Exception as error:
|
|
292
|
+
raise CreatePartialFailure(
|
|
293
|
+
incomplete, info=info, phase="inspect", cause=error,
|
|
294
|
+
) from error
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
_BACKGROUND_BLOCK_MAX_BYTES = 64 * 1024
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
_BACKGROUND_TEXT_MAX_CHARS = 1024
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
_BACKGROUND_TEXT_FIELDS = (
|
|
304
|
+
"command", "description", "output_file", "summary", "last_event",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
_BACKGROUND_FACT_KEYS = (
|
|
309
|
+
"background_tasks",
|
|
310
|
+
"background_task_count",
|
|
311
|
+
"running_background_task_count",
|
|
312
|
+
"open_background_task_count",
|
|
313
|
+
"active_background_task_count",
|
|
314
|
+
"stale_background_task_count",
|
|
315
|
+
"background_tasks_truncated",
|
|
316
|
+
"background_task_limit",
|
|
317
|
+
"background_task_hint",
|
|
318
|
+
"background_task_summary",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _background_block_size(block: dict[str, Any]) -> int:
|
|
323
|
+
return len(json.dumps(
|
|
324
|
+
block, ensure_ascii=False, separators=(",", ":"), sort_keys=True,
|
|
325
|
+
default=str,
|
|
326
|
+
).encode("utf-8"))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def recorded_background_facts(snapshot: dict) -> dict[str, Any]:
|
|
330
|
+
"""Facts from the owner's recorded summary: its bounded items are the list.
|
|
331
|
+
|
|
332
|
+
The recorded items are the compact owner view (id, kind, status, label...),
|
|
333
|
+
not the detailed transcript inventory that ``CcxClient.background_tasks()``
|
|
334
|
+
returns; ``background_tasks_truncated`` reflects the summary's own bound.
|
|
335
|
+
"""
|
|
336
|
+
items = snapshot.get("items")
|
|
337
|
+
items = [dict(item) for item in items if isinstance(item, dict)] if isinstance(items, list) else []
|
|
338
|
+
return {
|
|
339
|
+
"background_task_summary": snapshot,
|
|
340
|
+
"background_tasks": items,
|
|
341
|
+
"background_tasks_truncated": bool(snapshot.get("items_truncated")),
|
|
342
|
+
"running_background_task_count": snapshot.get("running_count",
|
|
343
|
+
snapshot.get("runningCount")),
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def background_task_block(
|
|
348
|
+
facts: dict[str, Any] | None, *, supported: bool
|
|
349
|
+
) -> dict[str, Any]:
|
|
350
|
+
"""Bound one ``_background_task_facts`` projection for local protocol.
|
|
351
|
+
|
|
352
|
+
Unsupported engines get only ``background_tasks_supported=false``: absent
|
|
353
|
+
counts are not an observed empty inventory. Identities and counts are kept
|
|
354
|
+
exactly; ``background_task_details_truncated`` reports text clipping or
|
|
355
|
+
detail omission, while ``background_tasks_truncated`` keeps meaning "the
|
|
356
|
+
list is not the whole inventory".
|
|
357
|
+
"""
|
|
358
|
+
if not supported:
|
|
359
|
+
return {"background_tasks_supported": False}
|
|
360
|
+
facts = facts or {}
|
|
361
|
+
block: dict[str, Any] = {"background_tasks_supported": True}
|
|
362
|
+
for key in _BACKGROUND_FACT_KEYS:
|
|
363
|
+
if key in facts:
|
|
364
|
+
block[key] = facts[key]
|
|
365
|
+
details_truncated = False
|
|
366
|
+
tasks = []
|
|
367
|
+
for task in block.get("background_tasks") or []:
|
|
368
|
+
if not isinstance(task, dict):
|
|
369
|
+
continue
|
|
370
|
+
task = dict(task)
|
|
371
|
+
for field_name in _BACKGROUND_TEXT_FIELDS:
|
|
372
|
+
value = task.get(field_name)
|
|
373
|
+
if isinstance(value, str) and len(value) > _BACKGROUND_TEXT_MAX_CHARS:
|
|
374
|
+
task[field_name] = value[:_BACKGROUND_TEXT_MAX_CHARS]
|
|
375
|
+
details_truncated = True
|
|
376
|
+
tasks.append(task)
|
|
377
|
+
block["background_tasks"] = tasks
|
|
378
|
+
hint = block.get("background_task_hint")
|
|
379
|
+
if isinstance(hint, str) and len(hint) > _BACKGROUND_TEXT_MAX_CHARS:
|
|
380
|
+
block["background_task_hint"] = hint[:_BACKGROUND_TEXT_MAX_CHARS]
|
|
381
|
+
details_truncated = True
|
|
382
|
+
block["background_task_details_truncated"] = details_truncated
|
|
383
|
+
# Over budget: drop text details oldest-first, then oldest entries.
|
|
384
|
+
for task in tasks:
|
|
385
|
+
if _background_block_size(block) <= _BACKGROUND_BLOCK_MAX_BYTES:
|
|
386
|
+
break
|
|
387
|
+
for field_name in _BACKGROUND_TEXT_FIELDS:
|
|
388
|
+
if task.get(field_name) is not None:
|
|
389
|
+
task[field_name] = None
|
|
390
|
+
block["background_task_details_truncated"] = True
|
|
391
|
+
while tasks and _background_block_size(block) > _BACKGROUND_BLOCK_MAX_BYTES:
|
|
392
|
+
tasks.pop(0)
|
|
393
|
+
block["background_tasks_truncated"] = True
|
|
394
|
+
return block
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
MAX_RESPONSE_ID = 128
|