@agentsdock/server 1.0.10-beta.6 → 1.0.11-beta.1

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.
@@ -0,0 +1,322 @@
1
+ """Bounded-memory projection of native JSONL, with exact raw-byte receipts.
2
+
3
+ Opaque media/tool fields stay in the provider-owned file. This reader validates
4
+ all JSON, including skipped strings, but only materializes history metadata and
5
+ text. Bounds apply to retained JSON/work, never to opaque payload/file bytes.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import codecs
10
+ import json
11
+ import re
12
+ import time
13
+ from dataclasses import dataclass
14
+ from typing import BinaryIO, Callable
15
+
16
+ CHUNK_BYTES = 64 * 1024
17
+ MAX_RETAINED_BYTES = 32 * 1024 * 1024
18
+ MAX_DEPTH = 96
19
+ MAX_NODES = 250_000
20
+ MAX_KEY_BYTES = 4096
21
+ MAX_SCALAR_BYTES = 1024
22
+ SCAN_SECONDS = 60.0
23
+ _SPECIAL = re.compile(rb'["\\\x00-\x1f]')
24
+ _NUMBER = re.compile(rb'-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\Z')
25
+ _HEX = frozenset(b'0123456789abcdefABCDEF')
26
+
27
+
28
+ class TranscriptBudgetExceeded(ValueError):
29
+ """Never treat exhausted work/memory as an ignorable malformed record."""
30
+
31
+ def at_record(self, number: int, start: int):
32
+ # A delta scan starts inside the file; do not call this a file line or
33
+ # a user-visible chat message number. Never include transcript content.
34
+ return TranscriptBudgetExceeded(
35
+ f'{self} Record {number:,} in this scan (source byte offset {start:,}).')
36
+
37
+
38
+ class InvalidRecord(ValueError):
39
+ pass
40
+
41
+
42
+ def scan_check(deadline: float, cancelled: Callable[[], bool] | None = None,
43
+ *, budget_seconds: float | None = None):
44
+ if cancelled is not None and cancelled():
45
+ raise TranscriptBudgetExceeded('transcript scan cancelled')
46
+ if time.monotonic() >= deadline:
47
+ limit = f' ({budget_seconds:g} seconds)' if budget_seconds is not None else ''
48
+ raise TranscriptBudgetExceeded(
49
+ f'Transcript scan exceeded its processing time budget{limit}. '
50
+ 'Reading stopped before completion; this is a processing timeout, not a file-size limit.')
51
+
52
+
53
+ def byte_amount(size: int) -> str:
54
+ mib = f' ({size / (1024 * 1024):.2f} MiB)' if size >= 1024 * 1024 else ''
55
+ return f'{size:,} bytes{mib}'
56
+
57
+
58
+ def opaque_path(path: tuple) -> bool:
59
+ # These fields never feed text_from_content, native user/assistant matching,
60
+ # ownership/turn metadata or goal/wake classification. Do not skip arbitrary
61
+ # `content`, `text`, `message`, thinking or unknown fields.
62
+ if path == ('payload', 'output'):
63
+ return True # Codex function/custom tool output, not assistant text.
64
+ for prefix in (('message', 'content'), ('payload', 'content')):
65
+ if len(path) > 3 and path[:2] == prefix and isinstance(path[2], int):
66
+ tail = path[3:]
67
+ if prefix == ('message', 'content') and tail in (('content',), ('input',)):
68
+ return True # Claude tool result/body and tool invocation arguments.
69
+ if tail in (('source', 'data'), ('image_url',), ('image_url', 'url')):
70
+ return True
71
+ return False
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class RawRecord:
76
+ stream: BinaryIO
77
+ start: int
78
+ end: int
79
+ complete: bool
80
+ blank: bool
81
+ malformed: bool
82
+ deadline: float
83
+ cancelled: Callable[[], bool] | None = None
84
+ number: int = 1
85
+ budget_seconds: float | None = None
86
+
87
+ def __len__(self):
88
+ return self.end - self.start
89
+
90
+ def update_hash(self, digest):
91
+ # Restore the parser's stream position; this also works on Windows,
92
+ # where pread is unavailable. Each iterator owns this file handle.
93
+ saved = self.stream.tell()
94
+ try:
95
+ self.stream.seek(self.start)
96
+ remaining = len(self)
97
+ while remaining:
98
+ scan_check(self.deadline, self.cancelled, budget_seconds=self.budget_seconds)
99
+ chunk = self.stream.read(min(CHUNK_BYTES, remaining))
100
+ if not chunk:
101
+ raise ValueError('transcript was truncated during record hashing')
102
+ digest.update(chunk)
103
+ remaining -= len(chunk)
104
+ except TranscriptBudgetExceeded as error:
105
+ raise error.at_record(self.number, self.start) from error
106
+ finally:
107
+ self.stream.seek(saved)
108
+
109
+
110
+
111
+ class _RecordParser:
112
+ def __init__(self, stream, end, deadline, cancelled, project, maximum, budget_seconds):
113
+ self.stream, self.end = stream, end
114
+ self.deadline, self.cancelled, self.project = deadline, cancelled, project
115
+ self.buffer, self.index, self.final, self.complete = b'', 0, False, False
116
+ self.retained, self.nodes, self.maximum = 0, 0, maximum
117
+ self.budget_seconds = budget_seconds
118
+
119
+ def fill(self):
120
+ if self.index < len(self.buffer):
121
+ return True
122
+ if self.final:
123
+ return False
124
+ scan_check(self.deadline, self.cancelled, budget_seconds=self.budget_seconds)
125
+ remaining = self.end - self.stream.tell()
126
+ self.buffer = self.stream.readline(min(CHUNK_BYTES, remaining)) if remaining > 0 else b''
127
+ self.index = 0
128
+ self.complete = self.buffer.endswith(b'\n')
129
+ self.final = self.complete or not self.buffer or self.stream.tell() >= self.end
130
+ return bool(self.buffer)
131
+
132
+ def peek(self):
133
+ return self.buffer[self.index] if self.fill() else None
134
+
135
+ def take(self):
136
+ value = self.peek()
137
+ if value is None:
138
+ raise InvalidRecord('incomplete JSON record')
139
+ self.index += 1
140
+ return value
141
+
142
+ def space(self):
143
+ while self.peek() in (9, 10, 13, 32):
144
+ self.index += 1
145
+
146
+ def reserve(self, size):
147
+ self.retained += size
148
+ if self.retained > self.maximum:
149
+ raise TranscriptBudgetExceeded(
150
+ 'Single transcript record retained text/metadata exceeds its import budget: '
151
+ f'observed at least {byte_amount(self.retained)}; limit {byte_amount(self.maximum)}. '
152
+ 'This includes accounting overhead, not the total conversation file size.')
153
+
154
+ def check_key_size(self, size):
155
+ if size > MAX_KEY_BYTES:
156
+ raise TranscriptBudgetExceeded(
157
+ 'Single transcript record contains an oversized JSON field name: '
158
+ f'observed at least {byte_amount(size)}; limit {byte_amount(MAX_KEY_BYTES)}.')
159
+
160
+ def string(self, retain, key=False):
161
+ if self.take() != 34:
162
+ raise InvalidRecord('JSON string expected')
163
+ parts = bytearray(b'"') if retain else None
164
+ decoder = codecs.getincrementaldecoder('utf-8')()
165
+ size = 0
166
+ while self.fill():
167
+ match = _SPECIAL.search(self.buffer, self.index)
168
+ stop = match.start() if match else len(self.buffer)
169
+ piece = self.buffer[self.index:stop]
170
+ decoder.decode(piece, final=False)
171
+ if retain:
172
+ self.reserve(len(piece)); parts.extend(piece)
173
+ size += len(piece)
174
+ if key:
175
+ self.check_key_size(size)
176
+ self.index = stop
177
+ if match is None:
178
+ continue
179
+ special = self.take()
180
+ if special == 34:
181
+ decoder.decode(b'', final=True)
182
+ if retain:
183
+ parts.append(34)
184
+ return json.loads(parts)
185
+ return None
186
+ if special != 92:
187
+ raise InvalidRecord('control character in JSON string')
188
+ # Escapes are ASCII boundaries: a partial UTF-8 codepoint here is
189
+ # invalid, including in a discarded field.
190
+ decoder.decode(b'', final=True)
191
+ decoder = codecs.getincrementaldecoder('utf-8')()
192
+ escape = self.take()
193
+ raw = bytes((92, escape))
194
+ if escape == 117:
195
+ suffix = bytes(self.take() for _ in range(4))
196
+ if any(value not in _HEX for value in suffix):
197
+ raise InvalidRecord('invalid JSON Unicode escape')
198
+ raw += suffix
199
+ elif escape not in b'"\\/bfnrt':
200
+ raise InvalidRecord('invalid JSON escape')
201
+ if retain:
202
+ self.reserve(len(raw)); parts.extend(raw)
203
+ size += len(raw)
204
+ if key:
205
+ self.check_key_size(size)
206
+ raise InvalidRecord('unterminated JSON string')
207
+
208
+ def value(self, path=(), retain=True, depth=0):
209
+ self.nodes += 1
210
+ if depth > MAX_DEPTH:
211
+ raise TranscriptBudgetExceeded(
212
+ 'Single transcript record exceeds the JSON nesting limit: '
213
+ f'observed depth {depth:,}; limit {MAX_DEPTH:,}.')
214
+ if self.nodes > MAX_NODES:
215
+ raise TranscriptBudgetExceeded(
216
+ 'Single transcript record exceeds the JSON value-count limit: '
217
+ f'observed at least {self.nodes:,} values; limit {MAX_NODES:,}.')
218
+ self.space()
219
+ if self.project and opaque_path(path):
220
+ retain = False
221
+ if retain:
222
+ self.reserve(64) # Bound object overhead as well as string bytes.
223
+ first = self.peek()
224
+ if first == 34:
225
+ return self.string(retain)
226
+ if first in (123, 91):
227
+ self.take()
228
+ obj = first == 123
229
+ result = ({} if obj else []) if retain else None
230
+ close = 125 if obj else 93
231
+ self.space()
232
+ if self.peek() == close:
233
+ self.take(); return result
234
+ index = 0
235
+ while True:
236
+ if obj:
237
+ self.space()
238
+ key = self.string(True, key=True)
239
+ self.space()
240
+ if self.take() != 58:
241
+ raise InvalidRecord('JSON colon expected')
242
+ else:
243
+ key = index
244
+ value = self.value(path+(key,), retain, depth+1)
245
+ if retain:
246
+ if obj: result[key] = value
247
+ else: result.append(value)
248
+ self.space()
249
+ delimiter = self.take()
250
+ if delimiter == close:
251
+ return result
252
+ if delimiter != 44:
253
+ raise InvalidRecord('JSON comma expected')
254
+ index += 1
255
+ token = bytearray()
256
+ while self.peek() is not None and self.peek() not in b' \r\n\t,]}':
257
+ token.append(self.take())
258
+ if len(token) > MAX_SCALAR_BYTES:
259
+ raise TranscriptBudgetExceeded(
260
+ 'Single transcript record contains an oversized JSON scalar (number or literal): '
261
+ f'observed at least {byte_amount(len(token))}; limit {byte_amount(MAX_SCALAR_BYTES)}.')
262
+ raw = bytes(token)
263
+ if raw not in (b'true', b'false', b'null', b'NaN', b'Infinity', b'-Infinity') and not _NUMBER.fullmatch(raw):
264
+ raise InvalidRecord('invalid JSON scalar')
265
+ return json.loads(raw) if retain else None
266
+
267
+ def drain(self):
268
+ while self.fill():
269
+ self.index = len(self.buffer)
270
+
271
+
272
+ def jsonl_records(stream: BinaryIO, *, end: int, project=True, strict=False,
273
+ require_newline=True, max_records=250_000, deadline=None,
274
+ cancelled=None, max_retained_bytes=MAX_RETAINED_BYTES):
275
+ """Yield (projected object or None, ending offset, RawRecord).
276
+
277
+ Malformed provider lines may be represented by None, as before. Resource
278
+ exhaustion always raises, so callers never commit a partial-success cursor.
279
+ A fixed end excludes concurrent appends; callers own file identity checks.
280
+ """
281
+ # Externally supplied deadlines can belong to a larger operation. Do not
282
+ # invent a duration for them; the caller can provide its own timeout detail.
283
+ budget_seconds = SCAN_SECONDS if deadline is None else None
284
+ deadline = time.monotonic() + SCAN_SECONDS if deadline is None else deadline
285
+ count = 0
286
+ while stream.tell() < end:
287
+ count += 1
288
+ start = stream.tell()
289
+ if count > max_records:
290
+ raise TranscriptBudgetExceeded(
291
+ 'Transcript scan exceeds the record-count limit: '
292
+ f'observed at least {count:,} records; limit {max_records:,}. '
293
+ 'This counts JSONL records, not chat messages.').at_record(count, start)
294
+ parser = _RecordParser(stream, end, deadline, cancelled, project, max_retained_bytes, budget_seconds)
295
+ blank = False
296
+ malformed = False
297
+ try:
298
+ try:
299
+ parser.space()
300
+ blank = parser.peek() is None
301
+ value = None if blank else parser.value()
302
+ parser.space()
303
+ if parser.peek() is not None:
304
+ raise InvalidRecord('trailing JSON content')
305
+ except (InvalidRecord, UnicodeDecodeError, json.JSONDecodeError) as error:
306
+ if strict:
307
+ raise ValueError('malformed transcript record') from error
308
+ parser.drain()
309
+ malformed = True
310
+ value = None
311
+ except TranscriptBudgetExceeded as error:
312
+ raise error.at_record(count, start) from error
313
+ stop = stream.tell()
314
+ if stop <= start:
315
+ raise ValueError('transcript was truncated during reading')
316
+ if require_newline and not parser.complete:
317
+ raise ValueError('transcript cursor does not end on a complete line')
318
+ raw = RawRecord(stream, start, stop, parser.complete, blank, malformed or blank,
319
+ deadline, cancelled, count, budget_seconds)
320
+ if strict and not isinstance(value, dict):
321
+ raise ValueError('transcript object expected')
322
+ yield value if isinstance(value, dict) else None, stop, raw