parawatch 0.3.17 → 0.3.18

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.
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { applyStall } from "./claudeCode.js";
5
- import { isObj, logActivity, oneLine, readHeadLines, readHeadText, readTailLines, str, textValues } from "./jsonl.js";
5
+ import { isObj, lastLineWith, logActivity, oneLine, readHeadLines, readHeadText, readTailLines, str, textValues } from "./jsonl.js";
6
6
  const HEAD_BYTES = 128 * 1024;
7
7
  const TAIL_BYTES = 256 * 1024;
8
8
  // prompts can sit behind several inline base64 images on one line; read this far only when needed
@@ -85,6 +85,19 @@ export function codexSettings(lines) {
85
85
  const options = [sandbox ? (SANDBOXES[sandbox] ?? sandbox) : undefined, approval ? (APPROVALS[approval] ?? approval) : undefined].filter((x) => !!x);
86
86
  return { model: str(tc.model), effort: str(tc.effort) ?? str(tc.reasoning_effort) ?? (isObj(tc.collaboration_mode) && isObj(tc.collaboration_mode.settings) ? str(tc.collaboration_mode.settings.reasoning_effort) : undefined), options: options.length ? options : undefined };
87
87
  }
88
+ /** What the last turn ran with, per log: a long log is searched once, then only where it grew. */
89
+ const middle = new Map();
90
+ /** The settings of a log whose last `turn_context` sits between its head and tail. */
91
+ export function settingsInMiddle(file) {
92
+ const size = fs.statSync(file).size;
93
+ const known = middle.get(file);
94
+ if (known && known.size === size)
95
+ return known.settings;
96
+ const found = lastLineWith(file, '"type":"turn_context"', known && known.size < size ? Math.max(0, known.size - 64) : 0);
97
+ const settings = found ? codexSettings([found]) : (known?.settings ?? {});
98
+ middle.set(file, { size, settings });
99
+ return settings;
100
+ }
88
101
  export function readCodexLog(head, tail, rawHead) {
89
102
  const meta = head.map(payload).find((p, i) => p && isObj(head[i]) && head[i].type === "session_meta");
90
103
  const id = str(meta?.id) ?? str(meta?.session_id) ?? "unknown";
@@ -188,6 +201,8 @@ export const codex = {
188
201
  if (fs.statSync(file).mtimeMs < opts.sinceMs)
189
202
  continue;
190
203
  const run = { ...readCodexLog(readHeadLines(file, HEAD_BYTES), readTailLines(file, TAIL_BYTES), () => readHeadText(file, RAW_HEAD_BYTES)), source: file };
204
+ if (!run.model)
205
+ Object.assign(run, settingsInMiddle(file));
191
206
  run.activity = logActivity(file, opts.now);
192
207
  applyStall(run, undefined, opts.now);
193
208
  runs.push(run);
@@ -141,4 +141,70 @@ export function logActivity(file, now) {
141
141
  seen.set(file, entry);
142
142
  return activeMinutes(entry.times, now);
143
143
  }
144
+ /**
145
+ * The last line holding `marker` in the part of a file after byte `from`, parsed, searching back
146
+ * from the end a megabyte at a time. For a record written once a turn that can sit anywhere in a
147
+ * log of many megabytes, between what the head and tail readings cover.
148
+ */
149
+ export function lastLineWith(file, marker, from = 0, maxLine = 4 * 1024 * 1024) {
150
+ const size = fs.statSync(file).size;
151
+ const needle = Buffer.from(marker);
152
+ const CHUNK = 1024 * 1024;
153
+ const fd = fs.openSync(file, "r");
154
+ try {
155
+ let end = size;
156
+ while (end > from) {
157
+ const start = Math.max(from, end - CHUNK);
158
+ // overlap by the marker's length, so one cut in two is still found
159
+ const len = Math.min(size, end + needle.length) - start;
160
+ const buf = Buffer.alloc(len);
161
+ const n = fs.readSync(fd, buf, 0, len, start);
162
+ const at = buf.subarray(0, n).lastIndexOf(needle);
163
+ if (at >= 0)
164
+ return lineAt(fd, size, start + at, maxLine);
165
+ end = start;
166
+ }
167
+ return undefined;
168
+ }
169
+ finally {
170
+ fs.closeSync(fd);
171
+ }
172
+ }
173
+ /** The whole line around a byte position, parsed; undefined when it is too long or not JSON. */
174
+ function lineAt(fd, size, pos, maxLine) {
175
+ const STEP = 64 * 1024;
176
+ let lineStart = pos;
177
+ while (lineStart > 0 && pos - lineStart < maxLine) {
178
+ const from = Math.max(0, lineStart - STEP);
179
+ const buf = Buffer.alloc(lineStart - from);
180
+ fs.readSync(fd, buf, 0, buf.length, from);
181
+ const nl = buf.lastIndexOf(10);
182
+ if (nl >= 0) {
183
+ lineStart = from + nl + 1;
184
+ break;
185
+ }
186
+ lineStart = from;
187
+ }
188
+ let lineEnd = pos;
189
+ while (lineEnd < size && lineEnd - lineStart < maxLine) {
190
+ const buf = Buffer.alloc(Math.min(STEP, size - lineEnd));
191
+ fs.readSync(fd, buf, 0, buf.length, lineEnd);
192
+ const nl = buf.indexOf(10);
193
+ if (nl >= 0) {
194
+ lineEnd += nl;
195
+ break;
196
+ }
197
+ lineEnd += buf.length;
198
+ }
199
+ if (lineEnd - lineStart >= maxLine)
200
+ return undefined;
201
+ const buf = Buffer.alloc(lineEnd - lineStart);
202
+ fs.readSync(fd, buf, 0, buf.length, lineStart);
203
+ try {
204
+ return JSON.parse(buf.toString("utf8"));
205
+ }
206
+ catch {
207
+ return undefined;
208
+ }
209
+ }
144
210
  //# sourceMappingURL=jsonl.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parawatch",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "A control tower for people running many AI coding agents in parallel: see what is running, what needs you, and wake the right agent when you answer.",
5
5
  "keywords": [
6
6
  "ai-agents",