@ponythewhite/base-context 1.0.15 → 1.0.16
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/CHANGELOG.md +9 -0
- package/dist/base-context-runtime/pyproject.toml +1 -1
- package/dist/base-context-runtime/src/rlm/bash.py +19 -0
- package/dist/base-context-runtime/src/rlm/job_watch.py +156 -0
- package/dist/base-context-runtime/src/rlm/repl.md +28 -6
- package/dist/base-context-runtime/src/rlm/repl.py +6 -1
- package/dist/base-context-runtime/test/test_bash.py +4 -4
- package/dist/base-context-runtime/test/test_job_watch.py +67 -0
- package/dist/base-context-runtime/uv.lock +1 -1
- package/dist/build-info.json +1 -1
- package/dist/bundle/{bundled-modules-O67MEO74.js → bundled-modules-2DMTK7VK.js} +2 -2
- package/dist/bundle/{chunk-RQFC34FM.js → chunk-MSH6QRP5.js} +1601 -820
- package/dist/bundle/{chunk-DGO66I3L.js → chunk-T2OK7SRV.js} +3 -2
- package/dist/bundle/{cli-main-2RER6PBE.js → cli-main-WL2L5IEQ.js} +2 -2
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/{main-YERLD3T4.js → main-XPIEIEQJ.js} +2 -2
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +155 -9
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/history-index-worker.d.ts.map +1 -1
- package/dist/core/history-index-worker.js +22 -1
- package/dist/core/history-index-worker.js.map +1 -1
- package/dist/core/history-index.d.ts +2 -0
- package/dist/core/history-index.d.ts.map +1 -1
- package/dist/core/history-index.js.map +1 -1
- package/dist/core/job-watch.d.ts +120 -0
- package/dist/core/job-watch.d.ts.map +1 -0
- package/dist/core/job-watch.js +648 -0
- package/dist/core/job-watch.js.map +1 -0
- package/dist/core/kernel/repl-manager.d.ts +5 -1
- package/dist/core/kernel/repl-manager.d.ts.map +1 -1
- package/dist/core/kernel/repl-manager.js +52 -0
- package/dist/core/kernel/repl-manager.js.map +1 -1
- package/dist/core/kernel/shared.d.ts +12 -0
- package/dist/core/kernel/shared.d.ts.map +1 -1
- package/dist/core/kernel/shared.js.map +1 -1
- package/dist/core/prompts/rlm.d.ts.map +1 -1
- package/dist/core/prompts/rlm.js +49 -39
- package/dist/core/prompts/rlm.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +9 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tools/prime-context.d.ts.map +1 -1
- package/dist/core/tools/prime-context.js +1 -1
- package/dist/core/tools/prime-context.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/skills/agent-message/SKILL.md +8 -2
- package/dist/skills/bounded-inspect/SKILL.md +78 -0
- package/dist/skills/bounded-inspect/pyproject.toml +16 -0
- package/dist/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
- package/dist/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
- package/dist/skills/job-watch/SKILL.md +43 -0
- package/dist/skills/job-watch/pyproject.toml +13 -0
- package/dist/skills/job-watch/src/job_watch/__init__.py +61 -0
- package/dist/skills/rlm-heartbeat/SKILL.md +10 -6
- package/dist/utils/file-lines.d.ts.map +1 -1
- package/dist/utils/file-lines.js +6 -2
- package/dist/utils/file-lines.js.map +1 -1
- package/docs/rlm-runtime.md +4 -1
- package/docs/skills.md +18 -26
- package/package.json +4 -4
- package/skills/agent-message/SKILL.md +8 -2
- package/skills/bounded-inspect/SKILL.md +78 -0
- package/skills/bounded-inspect/pyproject.toml +16 -0
- package/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
- package/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
- package/skills/job-watch/SKILL.md +43 -0
- package/skills/job-watch/pyproject.toml +13 -0
- package/skills/job-watch/src/job_watch/__init__.py +61 -0
- package/skills/rlm-heartbeat/SKILL.md +10 -6
|
@@ -62,7 +62,8 @@ if child is not None:
|
|
|
62
62
|
|
|
63
63
|
## Result capsules
|
|
64
64
|
|
|
65
|
-
Use `send()` for short
|
|
65
|
+
Use `send()` for short steering, blockers, and decisions. Use `send_result()`
|
|
66
|
+
for substantial findings or a final public report, not for a short message:
|
|
66
67
|
|
|
67
68
|
```python
|
|
68
69
|
from pathlib import Path
|
|
@@ -74,7 +75,9 @@ receipt = await agent_message.send_result(
|
|
|
74
75
|
)
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
State
|
|
78
|
+
State the outcome, important caveats, decisions, and evidence references in the
|
|
79
|
+
capsule. Child findings are evidence, not independently verified truth. Retrieve
|
|
80
|
+
details only when needed. If it revises or
|
|
78
81
|
replaces earlier reports, name their exact report refs in the summary. With
|
|
79
82
|
several updates, skim the capsules first, then retrieve the last relevant
|
|
80
83
|
report or reports. Older reports remain readable. Do not treat unrelated
|
|
@@ -83,6 +86,9 @@ arrived. There is no automatic latest-wins, dropping, or coalescing.
|
|
|
83
86
|
|
|
84
87
|
## Safety
|
|
85
88
|
|
|
89
|
+
- Avoid routine acknowledgements and repeated status broadcasts. Messaging an
|
|
90
|
+
idle child can start another turn; not every message needs a reply.
|
|
91
|
+
|
|
86
92
|
- Do not delete a child immediately after `send`: delivered follow-ups may still
|
|
87
93
|
be running and queued receipts have not run yet. Wait until observation shows
|
|
88
94
|
the child is idle and its context is no longer needed before calling
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bounded-inspect
|
|
3
|
+
description: Inspect selected text windows and JSON fields from project files under shared source and serialized-output byte budgets. Emit one source-linked bundle; continue within oversized lines without repeating full reads.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Bounded Inspect
|
|
7
|
+
|
|
8
|
+
Use for repeated, selective file inspection. Keep tiny one-off reads inline.
|
|
9
|
+
Historical conversation evidence belongs to `prime_context`, not this skill.
|
|
10
|
+
Filesystem paths and byte cursors are **not native recovery refs**.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
bounded_inspect.emit(
|
|
14
|
+
bounded_inspect.text("runs/example/train.log", tail_lines=25),
|
|
15
|
+
bounded_inspect.json_fields("runs/example/metrics.json",
|
|
16
|
+
pointers=["/run", "/config", "/seed", "/eligible", "/failures",
|
|
17
|
+
"/units", "/updates", "/skips", "/wall"]),
|
|
18
|
+
max_bytes=4096, max_source_bytes=65536,
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Selectors are lazy; only `emit` reads. It prints one JSON object and returns
|
|
23
|
+
`None`, so a final cell expression does not duplicate the result. Stateless
|
|
24
|
+
extraction works in ordinary, goal, and AUTONOMOUS sessions. It does not park,
|
|
25
|
+
finish, or change autonomous limits. Consume the supplied evidence; do not
|
|
26
|
+
rerun its generating command merely to get a smaller answer.
|
|
27
|
+
|
|
28
|
+
## API and coverage
|
|
29
|
+
|
|
30
|
+
- `text(path)` selects the first 25 lines. Choose one of `head_lines=N`,
|
|
31
|
+
`tail_lines=N`, `start_line=N, end_line=M`, or `cursor=item["next"]`.
|
|
32
|
+
Line ranges are 1-based and inclusive; omitted `end_line` means through EOF.
|
|
33
|
+
- `json_fields(path, pointers=[...], max_source_bytes=65536)` uses exact JSON
|
|
34
|
+
pointers, including `~0` and `~1` escapes. An empty pointer selects the root.
|
|
35
|
+
Null stays null. Missing fields, invalid JSON, unavailable files, and oversized
|
|
36
|
+
values have distinct statuses. Selected values are never string-sliced. Numeric
|
|
37
|
+
tokens stay exact, including exponents and nested values; no float conversion.
|
|
38
|
+
- `emit(*selectors, max_bytes=4096, max_source_bytes=65536)` shares both budgets
|
|
39
|
+
across all selectors, in order. `max_bytes` must be at least 128 for refusal
|
|
40
|
+
metadata. Raise explicit limits when necessary, not by an unbounded retry.
|
|
41
|
+
|
|
42
|
+
`output_bytes` counts the complete UTF-8 JSON serialization, escaping, metadata,
|
|
43
|
+
paths, warnings, and final newline. `source_bytes` counts bytes actually read,
|
|
44
|
+
not file size or OS device traffic. The enclosing tool/protocol envelope is
|
|
45
|
+
outside this budget; normal `ipython` retention still applies. `omitted` identifies
|
|
46
|
+
unreturned selector indices in the original call; `omitted_fields` counts the
|
|
47
|
+
unreturned suffix of requested pointers. Order important metadata first.
|
|
48
|
+
|
|
49
|
+
Text includes a stat version, half-open byte range, starting line (or null),
|
|
50
|
+
and next byte cursor. Partial Unicode lines advance at UTF-8 boundaries.
|
|
51
|
+
A continuation advances, finishes, or explicitly refuses. Source-limited line
|
|
52
|
+
seeking can return an empty window with an advancing cursor. Reuse the same
|
|
53
|
+
path and returned cursor; modification or replacement returns `changed_source`.
|
|
54
|
+
Stat tokens are practical guards, not immutable captures.
|
|
55
|
+
|
|
56
|
+
Tail reads only a bounded suffix. If it cannot establish the requested start,
|
|
57
|
+
`tail_prefix_unread` marks missing prefix coverage; terminal `tail_window_limit`
|
|
58
|
+
means that coverage is incomplete. Absolute line numbers may be unknown. JSON parsing refuses files above the explicit source ceiling before
|
|
59
|
+
reading. Large histories need a project-native summary producer, not larger
|
|
60
|
+
unbounded `json.load` calls. Select scientific units, run/config/seed identifiers,
|
|
61
|
+
eligibility, and failures together. Extraction does not infer quality crossings,
|
|
62
|
+
repair failed controls, or turn unavailable measurements into zero.
|
|
63
|
+
|
|
64
|
+
## Run where the data lives
|
|
65
|
+
|
|
66
|
+
Use `python -m bounded_inspect`, or stage `src/bounded_inspect/__init__.py` once
|
|
67
|
+
and execute that file with standard-library Python through existing authorized
|
|
68
|
+
remote execution. Pass a JSON list on stdin:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
[{"kind":"text","path":"train.log","tail_lines":25},
|
|
72
|
+
{"kind":"json","path":"metrics.json","pointers":["/run","/units","/wall"]}]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
CLI flags: `--max-bytes` and `--max-source-bytes`. No SSH setup is provided.
|
|
76
|
+
Save a small parameterized project parser after demonstrated reuse. Log full
|
|
77
|
+
execution evidence to explicit files when needed: this skill cannot recover
|
|
78
|
+
bytes that Bash or another capture already discarded.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bounded-inspect"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Bounded text and selected JSON file evidence"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = []
|
|
7
|
+
|
|
8
|
+
[project.scripts]
|
|
9
|
+
bounded-inspect = "bounded_inspect:main"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/bounded_inspect"]
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Bounded file evidence. Select lazily; emit one shared-budget JSON bundle."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
__all__ = ["text", "json_fields", "emit"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class _Selection:
|
|
15
|
+
kind: str
|
|
16
|
+
path: str
|
|
17
|
+
options: dict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def text(path, *, head_lines=None, tail_lines=None, start_line=None, end_line=None, cursor=None):
|
|
21
|
+
"""Select UTF-8 lines (1-based, inclusive), or resume a returned byte cursor."""
|
|
22
|
+
modes = sum(value is not None for value in (head_lines, tail_lines, start_line, cursor))
|
|
23
|
+
if modes > 1 or (end_line is not None and start_line is None):
|
|
24
|
+
raise ValueError("choose head, tail, line range, or cursor")
|
|
25
|
+
for value in (head_lines, tail_lines, start_line, end_line):
|
|
26
|
+
if value is not None and (not isinstance(value, int) or value < 1):
|
|
27
|
+
raise ValueError("line counts must be positive integers")
|
|
28
|
+
if start_line is not None and end_line is not None and end_line < start_line:
|
|
29
|
+
raise ValueError("end_line precedes start_line")
|
|
30
|
+
if modes == 0:
|
|
31
|
+
head_lines = 25
|
|
32
|
+
return _Selection("text", os.fspath(path), dict(head=head_lines, tail=tail_lines,
|
|
33
|
+
start=start_line, end=end_line, cursor=dict(cursor) if cursor else cursor))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def json_fields(path, *, pointers, max_source_bytes=65536):
|
|
37
|
+
"""Select exact RFC 6901 pointers; missing is distinct from JSON null."""
|
|
38
|
+
pointers = tuple(pointers)
|
|
39
|
+
if any(not isinstance(p, str) or (p and not p.startswith("/")) for p in pointers):
|
|
40
|
+
raise ValueError("pointers must be empty (root) or start with /")
|
|
41
|
+
if max_source_bytes < 0:
|
|
42
|
+
raise ValueError("max_source_bytes must be nonnegative")
|
|
43
|
+
return _Selection("json", os.fspath(path), dict(pointers=pointers, cap=max_source_bytes))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _version(info):
|
|
47
|
+
return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _JsonNumber(str):
|
|
51
|
+
"""A number token validated by json.loads, not converted through float/int."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _serialized(value):
|
|
55
|
+
if isinstance(value, _JsonNumber):
|
|
56
|
+
return str(value)
|
|
57
|
+
if isinstance(value, dict):
|
|
58
|
+
return "{" + ",".join(_serialized(key) + ":" + _serialized(item) for key, item in value.items()) + "}"
|
|
59
|
+
if isinstance(value, list):
|
|
60
|
+
return "[" + ",".join(_serialized(item) for item in value) + "]"
|
|
61
|
+
return json.dumps(value, ensure_ascii=True, allow_nan=False, separators=(",", ":"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _encoded(bundle):
|
|
65
|
+
# Count the complete wire envelope, including escaping and the final newline.
|
|
66
|
+
while True:
|
|
67
|
+
rendered = _serialized(bundle) + "\n"
|
|
68
|
+
size = len(rendered.encode("utf-8"))
|
|
69
|
+
if bundle["output_bytes"] == size:
|
|
70
|
+
return rendered
|
|
71
|
+
bundle["output_bytes"] = size
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _read(handle, count, bundle):
|
|
75
|
+
data = handle.read(max(0, min(count, bundle["limits"]["source_bytes"] - bundle["source_bytes"])))
|
|
76
|
+
bundle["source_bytes"] += len(data)
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cursor(path, version, offset, line, start, end, warnings):
|
|
81
|
+
return dict(path=path, version=version, offset=offset, line=line,
|
|
82
|
+
start_line=start, end_line=end, warnings=warnings)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _text(handle, selection, version, bundle):
|
|
86
|
+
options = selection.options
|
|
87
|
+
size = version[2]
|
|
88
|
+
cap = bundle["limits"]["source_bytes"] - bundle["source_bytes"]
|
|
89
|
+
cursor = options["cursor"]
|
|
90
|
+
warnings = []
|
|
91
|
+
offset, line = 0, 1
|
|
92
|
+
start, end = options["start"] or 1, options["end"] or options["head"]
|
|
93
|
+
if cursor is not None:
|
|
94
|
+
if cursor.get("path") != selection.path or cursor.get("version") != version:
|
|
95
|
+
return {"status": "changed_source", "next": None}, None
|
|
96
|
+
try:
|
|
97
|
+
offset, line = cursor["offset"], cursor["line"]
|
|
98
|
+
start, end = cursor["start_line"], cursor["end_line"]
|
|
99
|
+
warnings = cursor["warnings"]
|
|
100
|
+
if not isinstance(offset, int) or not 0 <= offset <= size:
|
|
101
|
+
raise ValueError()
|
|
102
|
+
except (KeyError, TypeError, ValueError):
|
|
103
|
+
return {"status": "invalid_cursor", "next": None}, None
|
|
104
|
+
elif options["tail"] is not None:
|
|
105
|
+
offset = max(0, size - cap)
|
|
106
|
+
line = 1 if offset == 0 else None
|
|
107
|
+
end = None
|
|
108
|
+
handle.seek(offset)
|
|
109
|
+
raw = _read(handle, size - offset, bundle)
|
|
110
|
+
if options["tail"] is not None:
|
|
111
|
+
# Work backwards only within the bounded suffix, never scan the prefix.
|
|
112
|
+
boundaries = [i + 1 for i, value in enumerate(raw) if value == 10 and i + 1 < len(raw)]
|
|
113
|
+
needed = options["tail"]
|
|
114
|
+
cut = boundaries[-needed] if len(boundaries) >= needed else 0
|
|
115
|
+
if offset and not cut:
|
|
116
|
+
warnings.append("tail_prefix_unread; absolute_line_unavailable")
|
|
117
|
+
while cut < len(raw) and raw[cut] & 0xC0 == 0x80:
|
|
118
|
+
cut += 1
|
|
119
|
+
if line is not None:
|
|
120
|
+
line += raw[:cut].count(b"\n")
|
|
121
|
+
offset += cut
|
|
122
|
+
raw = raw[cut:]
|
|
123
|
+
# Seek a requested line in bounded chunks. Cursors can advance without text.
|
|
124
|
+
if line is not None:
|
|
125
|
+
while line < start:
|
|
126
|
+
newline = raw.find(b"\n")
|
|
127
|
+
if newline < 0:
|
|
128
|
+
offset += len(raw)
|
|
129
|
+
raw = b""
|
|
130
|
+
break
|
|
131
|
+
offset += newline + 1
|
|
132
|
+
raw = raw[newline + 1:]
|
|
133
|
+
line += 1
|
|
134
|
+
if end is not None:
|
|
135
|
+
limit = end - line + 1
|
|
136
|
+
if limit <= 0:
|
|
137
|
+
raw = b""
|
|
138
|
+
else:
|
|
139
|
+
pos = 0
|
|
140
|
+
for _ in range(limit):
|
|
141
|
+
newline = raw.find(b"\n", pos)
|
|
142
|
+
if newline < 0:
|
|
143
|
+
break
|
|
144
|
+
pos = newline + 1
|
|
145
|
+
else:
|
|
146
|
+
raw = raw[:pos]
|
|
147
|
+
try:
|
|
148
|
+
decoded = raw.decode("utf-8")
|
|
149
|
+
except UnicodeDecodeError as error:
|
|
150
|
+
if error.reason != "unexpected end of data" or offset + len(raw) == size:
|
|
151
|
+
return {"status": "invalid_utf8", "byte": offset + error.start, "next": None}, None
|
|
152
|
+
decoded = raw[:error.start].decode("utf-8")
|
|
153
|
+
context = (selection.path, version, offset, line, start, end, warnings)
|
|
154
|
+
if not decoded:
|
|
155
|
+
seeking = line is not None and line < start and offset < size
|
|
156
|
+
done = offset == size or (end is not None and line is not None and line > end)
|
|
157
|
+
status = "complete" if done else "seeking" if seeking and raw == b"" and cap else "source_limit"
|
|
158
|
+
next_cursor = _cursor(*context) if status == "seeking" else None
|
|
159
|
+
return dict(status=status, text="", range={"bytes": [offset, offset], "line": line},
|
|
160
|
+
next=next_cursor, warnings=warnings), None
|
|
161
|
+
return {}, (decoded, context)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _text_prefix(decoded, context, length):
|
|
165
|
+
path, version, offset, line, start, end, warnings = context
|
|
166
|
+
value = decoded[:length]
|
|
167
|
+
stop = offset + len(value.encode("utf-8"))
|
|
168
|
+
next_line = None if line is None else line + value.count("\n")
|
|
169
|
+
done = stop == version[2] or (end is not None and next_line is not None and next_line > end)
|
|
170
|
+
status = ("tail_window_limit" if warnings else "complete") if done else "partial" if length else "output_limit"
|
|
171
|
+
following = None if done or not length else _cursor(path, version, stop, next_line, start, end, warnings)
|
|
172
|
+
return dict(status=status, text=value, range={"bytes": [offset, stop], "line": line},
|
|
173
|
+
next=following, warnings=warnings)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _constant(value):
|
|
177
|
+
raise ValueError("nonstandard JSON constant")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _json(handle, selection, version, bundle):
|
|
181
|
+
cap = min(selection.options["cap"], bundle["limits"]["source_bytes"] - bundle["source_bytes"])
|
|
182
|
+
if version[2] > cap:
|
|
183
|
+
return {"status": "source_limit", "size_bytes": version[2], "source_ceiling": cap}, None
|
|
184
|
+
raw = _read(handle, version[2], bundle)
|
|
185
|
+
try:
|
|
186
|
+
data = json.loads(raw, parse_float=_JsonNumber, parse_int=_JsonNumber, parse_constant=_constant)
|
|
187
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
188
|
+
return {"status": "invalid_json"}, None
|
|
189
|
+
fields = []
|
|
190
|
+
for pointer in selection.options["pointers"]:
|
|
191
|
+
value = data
|
|
192
|
+
try:
|
|
193
|
+
for part in pointer.split("/")[1:] if pointer else []:
|
|
194
|
+
if any(part[i:i + 2] not in ("~0", "~1") for i, ch in enumerate(part) if ch == "~"):
|
|
195
|
+
raise ValueError()
|
|
196
|
+
part = part.replace("~1", "/").replace("~0", "~")
|
|
197
|
+
if isinstance(value, list):
|
|
198
|
+
if not part.isascii() or not part.isdigit() or (len(part) > 1 and part[0] == "0"):
|
|
199
|
+
raise KeyError()
|
|
200
|
+
value = value[int(part)]
|
|
201
|
+
elif isinstance(value, dict):
|
|
202
|
+
value = value[part]
|
|
203
|
+
else:
|
|
204
|
+
raise KeyError()
|
|
205
|
+
fields.append(dict(pointer=pointer, status="ok", value=value))
|
|
206
|
+
except (KeyError, IndexError):
|
|
207
|
+
fields.append(dict(pointer=pointer, status="missing"))
|
|
208
|
+
except ValueError:
|
|
209
|
+
fields.append(dict(pointer=pointer, status="invalid_pointer"))
|
|
210
|
+
return {"status": "complete", "fields": [], "omitted_fields": len(fields)}, fields
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def emit(*selections, max_bytes=4096, max_source_bytes=65536):
|
|
214
|
+
"""Print one JSON bundle, within shared source/output byte limits. Return None."""
|
|
215
|
+
if not isinstance(max_bytes, int) or max_bytes < 128:
|
|
216
|
+
raise ValueError("max_bytes must be at least 128 (refusal envelope)")
|
|
217
|
+
if not isinstance(max_source_bytes, int) or max_source_bytes < 0:
|
|
218
|
+
raise ValueError("max_source_bytes must be nonnegative")
|
|
219
|
+
if any(not isinstance(selection, _Selection) for selection in selections):
|
|
220
|
+
raise TypeError("emit accepts text/json_fields selectors")
|
|
221
|
+
bundle = dict(items=[], omitted={"from": 0, "count": len(selections), "reason": "output_budget"},
|
|
222
|
+
source_bytes=0, output_bytes=0,
|
|
223
|
+
limits={"output_bytes": max_bytes, "source_bytes": max_source_bytes})
|
|
224
|
+
if len(_encoded(bundle).encode("utf-8")) > max_bytes:
|
|
225
|
+
bundle = dict(status="output_limit", omitted_count=len(selections), source_bytes=0, output_bytes=0)
|
|
226
|
+
print(_encoded(bundle), end="")
|
|
227
|
+
return None
|
|
228
|
+
for index, selection in enumerate(selections):
|
|
229
|
+
before = bundle["source_bytes"]
|
|
230
|
+
item = dict(index=index, path=selection.path, kind=selection.kind)
|
|
231
|
+
details = None
|
|
232
|
+
try:
|
|
233
|
+
# Unbuffered regular files: the counter is actual bytes read, not requested bytes.
|
|
234
|
+
with open(selection.path, "rb", buffering=0) as handle:
|
|
235
|
+
info = os.fstat(handle.fileno())
|
|
236
|
+
if not stat.S_ISREG(info.st_mode):
|
|
237
|
+
result = {"status": "not_regular_file"}
|
|
238
|
+
else:
|
|
239
|
+
version = _version(info)
|
|
240
|
+
item["version"] = version
|
|
241
|
+
if selection.kind == "text":
|
|
242
|
+
result, details = _text(handle, selection, version, bundle)
|
|
243
|
+
else:
|
|
244
|
+
result, details = _json(handle, selection, version, bundle)
|
|
245
|
+
if _version(os.fstat(handle.fileno())) != version or _version(os.stat(selection.path)) != version:
|
|
246
|
+
result, details = {"status": "changed_source", "next": None}, None
|
|
247
|
+
item.update(result)
|
|
248
|
+
except OSError as error:
|
|
249
|
+
item.update(status="unavailable", error=error.__class__.__name__, errno=error.errno)
|
|
250
|
+
item["source_bytes"] = bundle["source_bytes"] - before
|
|
251
|
+
bundle["items"].append(item)
|
|
252
|
+
bundle["omitted"].update({"from": index + 1, "count": len(selections) - index - 1})
|
|
253
|
+
|
|
254
|
+
def fits():
|
|
255
|
+
return len(_encoded(bundle).encode("utf-8")) <= max_bytes
|
|
256
|
+
|
|
257
|
+
if details is not None and selection.kind == "text":
|
|
258
|
+
decoded, context = details
|
|
259
|
+
item.update(_text_prefix(decoded, context, len(decoded)))
|
|
260
|
+
if not fits():
|
|
261
|
+
low, high = 0, len(decoded)
|
|
262
|
+
while low < high:
|
|
263
|
+
middle = (low + high + 1) // 2
|
|
264
|
+
item.update(_text_prefix(decoded, context, middle))
|
|
265
|
+
if fits():
|
|
266
|
+
low = middle
|
|
267
|
+
else:
|
|
268
|
+
high = middle - 1
|
|
269
|
+
item.update(_text_prefix(decoded, context, low))
|
|
270
|
+
elif details is not None:
|
|
271
|
+
for field_index, field in enumerate(details):
|
|
272
|
+
item["fields"].append(field)
|
|
273
|
+
item["omitted_fields"] = len(details) - field_index - 1
|
|
274
|
+
if not fits():
|
|
275
|
+
item["fields"][-1] = dict(pointer=field["pointer"], status="value_limit")
|
|
276
|
+
item["status"] = "partial"
|
|
277
|
+
if not fits():
|
|
278
|
+
item["fields"].pop()
|
|
279
|
+
item["omitted_fields"] += 1
|
|
280
|
+
break
|
|
281
|
+
if not fits():
|
|
282
|
+
bundle["items"].pop()
|
|
283
|
+
bundle["omitted"].update({"from": index, "count": len(selections) - index})
|
|
284
|
+
break
|
|
285
|
+
# Budget refusal may follow real I/O, so keep those source bytes in the envelope.
|
|
286
|
+
rendered = _encoded(bundle)
|
|
287
|
+
if len(rendered.encode("utf-8")) > max_bytes:
|
|
288
|
+
bundle = dict(status="output_limit", omitted_count=len(selections),
|
|
289
|
+
source_bytes=bundle["source_bytes"], output_bytes=0)
|
|
290
|
+
rendered = _encoded(bundle)
|
|
291
|
+
print(rendered, end="")
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def main():
|
|
296
|
+
"""Standalone: stage this file once, then pass a small JSON request on stdin."""
|
|
297
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
298
|
+
parser.add_argument("--max-bytes", type=int, default=4096)
|
|
299
|
+
parser.add_argument("--max-source-bytes", type=int, default=65536)
|
|
300
|
+
args = parser.parse_args()
|
|
301
|
+
request = json.load(sys.stdin)
|
|
302
|
+
selections = []
|
|
303
|
+
for entry in request:
|
|
304
|
+
entry = dict(entry)
|
|
305
|
+
kind, path = entry.pop("kind"), entry.pop("path")
|
|
306
|
+
if kind not in ("text", "json"):
|
|
307
|
+
parser.error("kind must be text or json")
|
|
308
|
+
selections.append((text if kind == "text" else json_fields)(path, **entry))
|
|
309
|
+
emit(*selections, max_bytes=args.max_bytes, max_source_bytes=args.max_source_bytes)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: job-watch
|
|
3
|
+
description: Watch launched jobs with native completion callbacks or scheduled read-only probes. Park owned continuation, suppress unchanged status, and deliver prepared evidence for failures, completion, changes, or required reports.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Job Watch
|
|
7
|
+
|
|
8
|
+
Use for repeated checks or jobs that outlive a short tool call. Tiny commands do not need a watch. Launch once with `bash()` or an authorized project helper. This skill never launches a job, retries a run, starts the next stage, or completes a goal.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
job = bash(existing_authorized_command)
|
|
12
|
+
w = await job_watch.watch(
|
|
13
|
+
job, job_id="run-1", completion_source="handle",
|
|
14
|
+
probe_command=existing_read_only_probe_command,
|
|
15
|
+
interval="5m", notify="terminal", report_every="5m", fields=["updates", "wall_seconds"],
|
|
16
|
+
)
|
|
17
|
+
await job_watch.park([w["id"]])
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Continue useful independent work instead of parking when possible. `park` returns immediately. It holds only automatic goal/autonomous continuation until an event from a selected watch. The goal stays active. User messages and unrelated queued work still run. End the turn after parking; do not poll or sleep.
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
- `await watch(job=None, *, job_id=None, completion_source, probe_command=None, interval="5m", notify="terminal", fields=None, report_every=None, deadline=None, probe_timeout=30)` registers once. Repeating the same active resource registration returns its existing watch. `fields` selects top-level progress keys for cached status, required reports, and `notify="changes"`. Include every metric needed in reports. Observation time never counts as a change. Intervals use existing schedule syntax, with a ten-second minimum. `deadline` is an ISO date; timeout is seconds.
|
|
25
|
+
- `await status(id=None)` returns cached snapshots, not a new probe.
|
|
26
|
+
- `await park(ids)` registers a wait without blocking the cell.
|
|
27
|
+
- `await unregister(id)` stops observation. It is idempotent and never kills the monitored job. It may cancel an in-flight probe.
|
|
28
|
+
|
|
29
|
+
Set `completion_source="probe"` for detached remote jobs. A successful SSH launcher exit is **not** remote completion. Probe-only registration requires `job_id`. A probe command must be an already authorized, noninteractive, read-only command in the project's native environment. Do not pass notebook closures. The command prints one JSON object of at most 16 KiB:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{"observed_at":"2026-09-28T12:00:00Z","job_id":"run-1","state":"running","progress":{"updates":12,"wall_seconds":45},"attention":[],"evidence":["runs/run-1/metrics.json"]}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use strings for exact identifiers and precision-sensitive values. Unsafe integers, nonfinite numbers, and precision-losing numeric tokens are rejected as unavailable observations.
|
|
36
|
+
|
|
37
|
+
States are `running`, `succeeded`, `failed`, `cancelled`, and `unknown`. Preserve units, configuration/run identity, unavailable values, and comparison eligibility in selected fields. Put project-specific threshold or quality-gate warnings in `attention`. Missing/partial JSON or transport failure means unavailable evidence, not permission to restart training. New observability problems and terminal failures remain visible; repeated identical problems are suppressed.
|
|
38
|
+
|
|
39
|
+
Checks do not require reports. Omit `report_every` unless the user requires periodic reports. Required reports include a prepared snapshot even when values did not change. Delayed observations show lateness; they do not pretend to have run on time. Notifications are about 2 KiB. Large fields are explicitly omitted and captured probe output remains in the supplied file locator. Bash may already have discarded output; use explicit project log files when complete execution evidence matters.
|
|
40
|
+
|
|
41
|
+
Consume delivered evidence; inspect raw sources only for decisions. Compaction does not recreate a watch. Clean shutdown retains undelivered reports with their original IDs. If shutdown interrupts delivery and its acknowledgement, recovery reports delivery as unknown; it does not claim exactly-once crash recovery. After restart, only an authoritative probe can reconcile a job; a lost handle becomes unknown. Never attach by PID or relaunch automatically. Imported/forked declarations do not acquire ownership. Preserve resource exclusivity, authorized run counts, and stop conditions. Unregister at the requested endpoint.
|
|
42
|
+
|
|
43
|
+
Pending watch state is limited to 64 KiB (less under smaller source limits). If undelivered evidence fills it, prior reports remain; monitoring stops with unknown, a reason, and a retained locator. This is not successful cadence preservation. The job keeps running. Resume delivery, unregister exhausted watches, and explicitly register again. No automatic retry.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "job-watch"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Base Context owned job monitoring over the kernel host bridge"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = []
|
|
7
|
+
|
|
8
|
+
[build-system]
|
|
9
|
+
requires = ["hatchling"]
|
|
10
|
+
build-backend = "hatchling.build"
|
|
11
|
+
|
|
12
|
+
[tool.hatch.build.targets.wheel]
|
|
13
|
+
packages = ["src/job_watch"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Monitor authorized jobs without model polling. Launch with bash(), not this skill."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
from rlm import host_request
|
|
5
|
+
from rlm.bash import BashHandle
|
|
6
|
+
try:
|
|
7
|
+
from rlm import job_watch as _runtime
|
|
8
|
+
except ImportError:
|
|
9
|
+
_runtime = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def watch(
|
|
13
|
+
job: BashHandle | None = None, *, job_id: str | None = None,
|
|
14
|
+
completion_source: Literal["handle", "probe"], probe_command: str | None = None,
|
|
15
|
+
interval: str = "5m", notify: Literal["terminal", "changes"] = "terminal",
|
|
16
|
+
fields: list[str] | None = None, report_every: str | None = None,
|
|
17
|
+
deadline: str | None = None, probe_timeout: float = 30,
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
"""Register once. Probe commands must be authorized, noninteractive, and read-only.
|
|
20
|
+
|
|
21
|
+
A probe prints JSON: observed_at, job_id, state, progress, attention, evidence.
|
|
22
|
+
fields selects top-level progress keys; timestamps never trigger progress changes.
|
|
23
|
+
completion_source='probe' is required for detached remote jobs, including SSH launchers.
|
|
24
|
+
"""
|
|
25
|
+
if _runtime is None:
|
|
26
|
+
raise RuntimeError("job-watch requires a runtime with job_watch_probe_v1; no watch was registered")
|
|
27
|
+
if completion_source not in {"handle", "probe"}:
|
|
28
|
+
raise ValueError("completion_source must be handle or probe")
|
|
29
|
+
if job is None and (completion_source == "handle" or not job_id):
|
|
30
|
+
raise ValueError("handle completion requires a BashHandle; probe-only watches require job_id")
|
|
31
|
+
resource = _runtime.identity(job) if job is not None else f"probe:{job_id}"
|
|
32
|
+
result = await host_request("job_watch.watch", {
|
|
33
|
+
"job_id": job_id or resource, "resource_id": resource,
|
|
34
|
+
"completion_source": completion_source, "probe_command": probe_command,
|
|
35
|
+
"interval": interval, "notify": notify, "fields": fields or [],
|
|
36
|
+
"report_every": report_every, "deadline": deadline, "timeout_ms": probe_timeout * 1000,
|
|
37
|
+
})
|
|
38
|
+
if job is not None and completion_source == "handle":
|
|
39
|
+
_runtime.bind(job, result["id"], result["generation"], job_id or resource)
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def status(id: str | None = None) -> dict[str, Any]:
|
|
44
|
+
"""Return cached state only; never launch a probe or a model turn."""
|
|
45
|
+
return await host_request("job_watch.status", {} if id is None else {"id": id})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def park(ids: list[str]) -> dict[str, Any]:
|
|
49
|
+
"""Declare no further owned continuation until a selected event. Returns immediately.
|
|
50
|
+
|
|
51
|
+
User messages and unrelated queued work still run. This does not pause the goal.
|
|
52
|
+
"""
|
|
53
|
+
return await host_request("job_watch.park", {"ids": ids})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def unregister(id: str) -> dict[str, Any]:
|
|
57
|
+
"""Stop watching, not the job. Repeated calls are harmless."""
|
|
58
|
+
result = await host_request("job_watch.unregister", {"id": id})
|
|
59
|
+
if _runtime is not None:
|
|
60
|
+
_runtime.unbind(id)
|
|
61
|
+
return result
|
|
@@ -12,8 +12,8 @@ replace, pause, resume, or clear that user-level heartbeat.
|
|
|
12
12
|
Call directly from the Python REPL:
|
|
13
13
|
|
|
14
14
|
```python
|
|
15
|
-
await rlm_heartbeat.create("
|
|
16
|
-
await rlm_heartbeat.create("
|
|
15
|
+
await rlm_heartbeat.create("review current priorities", interval="30m", label="review")
|
|
16
|
+
await rlm_heartbeat.create("review pending decisions", delivery_mode="follow_up")
|
|
17
17
|
await rlm_heartbeat.list()
|
|
18
18
|
await rlm_heartbeat.update("job-id", status="pause")
|
|
19
19
|
await rlm_heartbeat.delete("job-id")
|
|
@@ -46,9 +46,13 @@ the session when it is busy:
|
|
|
46
46
|
|
|
47
47
|
- Use this when the user asks you to start, create, schedule, or manage your own
|
|
48
48
|
heartbeat without explicitly referring to `/heartbeat`.
|
|
49
|
-
- Use this
|
|
50
|
-
|
|
49
|
+
- Use this for recurring work that needs a model decision. For deterministic
|
|
50
|
+
job checks, prefer `job-watch` when available: it probes before deciding whether
|
|
51
|
+
to wake the model. Preserve every required report and any explicit heartbeat
|
|
52
|
+
request; do not silently replace the user's schedule.
|
|
51
53
|
- Do not use this skill to satisfy a user's request to configure `/heartbeat`;
|
|
52
54
|
that is a separate user-level surface.
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
+
- Name the current authoritative state and an existing parameterized project
|
|
56
|
+
helper in monitoring instructions. Do not copy the same detailed status into
|
|
57
|
+
heartbeats, messages, notebook variables, and status documents. Reuse the
|
|
58
|
+
helper with current inputs; repetition never authorizes new jobs.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-lines.d.ts","sourceRoot":"","sources":["../../src/utils/file-lines.ts"],"names":[],"mappings":"AAEA,wBAAgB,uBAAuB,CACtC,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAY,EACpB,eAAe,UAAQ,GACrB,MAAM,GAAG,SAAS,CAsBpB;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAY,EAAE,YAAY,UAAQ,GAAG,MAAM,GAAG,SAAS,CAKlH;AAED,wBAAuB,kBAAkB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACvF,cAAc,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"file-lines.d.ts","sourceRoot":"","sources":["../../src/utils/file-lines.ts"],"names":[],"mappings":"AAEA,wBAAgB,uBAAuB,CACtC,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAY,EACpB,eAAe,UAAQ,GACrB,MAAM,GAAG,SAAS,CAsBpB;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAY,EAAE,YAAY,UAAQ,GAAG,MAAM,GAAG,SAAS,CAKlH;AAED,wBAAuB,kBAAkB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACvF,cAAc,CAAC,MAAM,CAAC,CAmCxB","sourcesContent":["import { closeSync, createReadStream, openSync, readSync } from \"node:fs\";\n\nexport function readFirstLineBufferSync(\n\tfilePath: string,\n\tmaxBytes = 64 * 1024,\n\tallowIncomplete = false,\n): Buffer | undefined {\n\tconst fd = openSync(filePath, \"r\");\n\tconst chunks: Buffer[] = [];\n\tlet position = 0;\n\ttry {\n\t\tconst buffer = Buffer.alloc(1024);\n\t\twhile (position < maxBytes) {\n\t\t\tconst count = readSync(fd, buffer, 0, Math.min(buffer.length, maxBytes - position), position);\n\t\t\tif (count === 0) break;\n\t\t\tconst chunk = buffer.subarray(0, count);\n\t\t\tconst newline = chunk.indexOf(0x0a);\n\t\t\tif (newline !== -1) {\n\t\t\t\tchunks.push(Buffer.from(chunk.subarray(0, newline + 1)));\n\t\t\t\treturn Buffer.concat(chunks);\n\t\t\t}\n\t\t\tchunks.push(Buffer.from(chunk));\n\t\t\tposition += count;\n\t\t}\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n\treturn allowIncomplete && chunks.length > 0 ? Buffer.concat(chunks) : undefined;\n}\n\nexport function readFirstLineSync(filePath: string, maxBytes = 64 * 1024, completeOnly = false): string | undefined {\n\treturn readFirstLineBufferSync(filePath, maxBytes, !completeOnly)\n\t\t?.toString(\"utf8\")\n\t\t.replace(/\\n$/, \"\")\n\t\t.replace(/\\r$/, \"\");\n}\n\nexport async function* readLinesAsBuffers(\n\tfilePath: string,\n\toptions: { maxLineBytes?: number; includeNewline?: boolean; completeOnly?: boolean } = {},\n): AsyncGenerator<Buffer> {\n\tconst maxBytes = options.maxLineBytes ?? Infinity;\n\tif (maxBytes <= 0 || (maxBytes !== Infinity && !Number.isSafeInteger(maxBytes)))\n\t\tthrow new Error(\"Invalid line byte limit\");\n\tconst pendingParts: Buffer[] = [];\n\tlet pendingBytes = 0;\n\tfor await (const chunk of createReadStream(filePath)) {\n\t\tconst buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n\t\tlet start = 0;\n\t\twhile (start < buffer.length) {\n\t\t\tconst end = buffer.indexOf(0x0a, start);\n\t\t\tconst stop = end === -1 ? buffer.length : end + (options.includeNewline ? 1 : 0);\n\t\t\tconst part = buffer.subarray(start, stop);\n\t\t\tif (pendingBytes + part.length > maxBytes) throw new Error(\"Line byte limit exceeded\");\n\t\t\tif (end === -1) {\n\t\t\t\tpendingParts.push(part);\n\t\t\t\tpendingBytes += part.length;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (pendingParts.length > 0) {\n\t\t\t\tpendingParts.push(part);\n\t\t\t\tconst line = Buffer.concat(pendingParts, pendingBytes + part.length);\n\t\t\t\tpendingParts.length = 0;\n\t\t\t\tpendingBytes = 0;\n\t\t\t\tyield line;\n\t\t\t} else yield part;\n\t\t\tstart = end + 1;\n\t\t}\n\t}\n\tif (pendingParts.length > 0 && !options.completeOnly) {\n\t\tconst line = Buffer.concat(pendingParts, pendingBytes);\n\t\tpendingParts.length = 0;\n\t\tpendingBytes = 0;\n\t\tyield line;\n\t}\n}\n"]}
|
package/dist/utils/file-lines.js
CHANGED
|
@@ -62,7 +62,11 @@ export async function* readLinesAsBuffers(filePath, options = {}) {
|
|
|
62
62
|
start = end + 1;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
if (pendingParts.length > 0 && !options.completeOnly)
|
|
66
|
-
|
|
65
|
+
if (pendingParts.length > 0 && !options.completeOnly) {
|
|
66
|
+
const line = Buffer.concat(pendingParts, pendingBytes);
|
|
67
|
+
pendingParts.length = 0;
|
|
68
|
+
pendingBytes = 0;
|
|
69
|
+
yield line;
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
//# sourceMappingURL=file-lines.js.map
|