@jentrix/runner 0.5.20 → 0.5.21

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/README.md CHANGED
@@ -132,10 +132,10 @@ Setup:
132
132
 
133
133
  | Context loaded up front | Bytes | ≈ Tokens |
134
134
  | --- | --- | --- |
135
- | MCP agent — all 242 tool definitions | 374 KB | ~101k |
135
+ | MCP agent — all 242 tool definitions | 375 KB | ~101k |
136
136
  | CLI agent — `CLI_STANDUP_SYSTEM_PROMPT` only | 2 KB | ~510 |
137
137
 
138
- That is a **~198× smaller** Jentrix-specific up-front context.
138
+ That is a **~199× smaller** Jentrix-specific up-front context.
139
139
  <!-- END GENERATED: mcp-tool-count:agent-token-cost -->
140
140
 
141
141
  Reproduce the two numbers directly:
@@ -0,0 +1,7 @@
1
+ // lib/version.ts
2
+ var RUNNER_VERSION = "0.5.21";
3
+
4
+ export {
5
+ RUNNER_VERSION
6
+ };
7
+ //# sourceMappingURL=chunk-UN63BYMS.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../lib/version.ts"],
4
- "sourcesContent": ["export const RUNNER_VERSION = \"0.5.20\";\n"],
4
+ "sourcesContent": ["export const RUNNER_VERSION = \"0.5.21\";\n"],
5
5
  "mappings": ";AAAO,IAAM,iBAAiB;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  RUNNER_VERSION
4
- } from "./chunk-FWAHAHAM.js";
4
+ } from "./chunk-UN63BYMS.js";
5
5
 
6
6
  // runner-cli.ts
7
7
  import { fileURLToPath } from "node:url";
@@ -219,7 +219,7 @@ async function main(args = process.argv.slice(2)) {
219
219
  return runConfiguredRunner(args.includes("--once"));
220
220
  }
221
221
  if (command === "session-run" && (args.includes("--plan-stdin") || args.includes("--plan-file"))) {
222
- const { runSessionHost } = await import("./session-host-P6W3PH3S.js");
222
+ const { runSessionHost } = await import("./session-host-L6XF4T2P.js");
223
223
  let raw;
224
224
  const planFile = valueAfter(args, "--plan-file");
225
225
  if (planFile) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RUNNER_VERSION
3
- } from "./chunk-FWAHAHAM.js";
3
+ } from "./chunk-UN63BYMS.js";
4
4
  import {
5
5
  appendHookEvent,
6
6
  readHookLines,
@@ -180,6 +180,126 @@ function serializeSessionEvent(event) {
180
180
  `;
181
181
  }
182
182
 
183
+ // lib/session-skeleton.ts
184
+ var SKELETON_VERSION = 1;
185
+ var MAX_SKELETON_BYTES = 32 * 1024;
186
+ var MAX_DISTINCT_TOOLS = 100;
187
+ var MAX_TRACKED_FILES = 300;
188
+ var MAX_HOURLY_BUCKETS = 500;
189
+ var MAX_PATH_CHARS = 300;
190
+ var PATH_KEYS = ["file_path", "path", "notebook_path", "filePath"];
191
+ function pathOf(value) {
192
+ if (typeof value !== "string") return null;
193
+ const trimmed = value.trim();
194
+ if (!trimmed || /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return null;
195
+ return trimmed.slice(0, MAX_PATH_CHARS);
196
+ }
197
+ var SessionSkeleton = class {
198
+ constructor(provider) {
199
+ this.provider = provider;
200
+ }
201
+ provider;
202
+ turns = 0;
203
+ firstEventAt = null;
204
+ lastEventAt = null;
205
+ eventCounts = /* @__PURE__ */ new Map();
206
+ toolCounts = /* @__PURE__ */ new Map();
207
+ files = /* @__PURE__ */ new Set();
208
+ filesOverflow = 0;
209
+ hourly = /* @__PURE__ */ new Map();
210
+ coalesced = false;
211
+ get observedAnything() {
212
+ return this.firstEventAt !== null;
213
+ }
214
+ /** Feed one REDACTED observed event. Pure accumulation, never throws. */
215
+ observe(event) {
216
+ this.eventCounts.set(
217
+ event.kind,
218
+ (this.eventCounts.get(event.kind) ?? 0) + 1
219
+ );
220
+ if (event.kind === "user_message") this.turns += 1;
221
+ if (this.firstEventAt === null) this.firstEventAt = event.at;
222
+ this.lastEventAt = event.at;
223
+ const hour = event.at.slice(0, 13);
224
+ if (this.hourly.has(hour) || this.hourly.size < MAX_HOURLY_BUCKETS) {
225
+ this.hourly.set(hour, (this.hourly.get(hour) ?? 0) + 1);
226
+ } else {
227
+ this.coalesced = true;
228
+ }
229
+ const payload = event.payload ?? {};
230
+ if (event.kind === "tool_call") {
231
+ const name = typeof payload.name === "string" && payload.name.trim() ? payload.name.trim().slice(0, 120) : "(unnamed)";
232
+ if (this.toolCounts.has(name) || this.toolCounts.size < MAX_DISTINCT_TOOLS) {
233
+ this.toolCounts.set(name, (this.toolCounts.get(name) ?? 0) + 1);
234
+ } else {
235
+ this.coalesced = true;
236
+ this.toolCounts.set(
237
+ "(other)",
238
+ (this.toolCounts.get("(other)") ?? 0) + 1
239
+ );
240
+ }
241
+ const input = payload.input;
242
+ if (input && typeof input === "object" && !Array.isArray(input)) {
243
+ for (const key of PATH_KEYS) {
244
+ const path = pathOf(input[key]);
245
+ if (path) this.touch(path);
246
+ }
247
+ }
248
+ }
249
+ if (event.kind === "file_change") {
250
+ const changes = payload.changes;
251
+ if (Array.isArray(changes)) {
252
+ for (const change of changes) {
253
+ const path = pathOf(change?.path);
254
+ if (path) this.touch(path);
255
+ }
256
+ } else if (changes && typeof changes === "object") {
257
+ for (const key of Object.keys(changes)) {
258
+ const path = pathOf(key);
259
+ if (path) this.touch(path);
260
+ }
261
+ }
262
+ }
263
+ }
264
+ touch(path) {
265
+ if (this.files.has(path)) return;
266
+ if (this.files.size >= MAX_TRACKED_FILES) {
267
+ this.filesOverflow += 1;
268
+ this.coalesced = true;
269
+ return;
270
+ }
271
+ this.files.add(path);
272
+ }
273
+ /** The bounded v1 JSON. ≤ 32 KB serialized — file-list tail drops first. */
274
+ snapshot() {
275
+ const build = (paths2, listTruncated2) => ({
276
+ version: SKELETON_VERSION,
277
+ provider: this.provider,
278
+ turns: this.turns,
279
+ firstEventAt: this.firstEventAt,
280
+ lastEventAt: this.lastEventAt,
281
+ eventCounts: Object.fromEntries(this.eventCounts),
282
+ toolCounts: Object.fromEntries(this.toolCounts),
283
+ filesTouched: {
284
+ total: this.files.size + this.filesOverflow,
285
+ paths: paths2,
286
+ ...listTruncated2 ? { listTruncated: true } : {}
287
+ },
288
+ hourlyBuckets: Object.fromEntries(this.hourly),
289
+ ...this.coalesced || listTruncated2 ? { truncated: true } : {}
290
+ });
291
+ let paths = [...this.files];
292
+ let listTruncated = this.filesOverflow > 0;
293
+ let skeleton = build(paths, listTruncated);
294
+ while (Buffer.byteLength(JSON.stringify(skeleton), "utf8") > MAX_SKELETON_BYTES && paths.length > 0) {
295
+ paths = paths.slice(0, Math.max(0, Math.floor(paths.length / 2)));
296
+ listTruncated = true;
297
+ skeleton = build(paths, listTruncated);
298
+ }
299
+ return skeleton;
300
+ }
301
+ };
302
+
183
303
  // lib/session-usage.ts
184
304
  function insideOneRange(ranges, from, to) {
185
305
  return ranges.some((r) => r.from <= from && to <= r.to);
@@ -362,6 +482,7 @@ var HEARTBEAT_MIN_INTERVAL_MS = 3e4;
362
482
  var SessionBridge = class {
363
483
  constructor(deps) {
364
484
  this.deps = deps;
485
+ this.skeleton = deps.collectSkeleton === false ? null : new SessionSkeleton(deps.provider);
365
486
  }
366
487
  deps;
367
488
  sequence = 0;
@@ -393,6 +514,13 @@ var SessionBridge = class {
393
514
  * session keeps its telemetry and loses what it actually concluded).
394
515
  */
395
516
  lastAssistantMessage = null;
517
+ /**
518
+ * Session evidence floor (PRD §4): the v1 activity skeleton, accumulated
519
+ * from every REDACTED event this bridge records — capture-off included —
520
+ * and submitted on the existing heartbeats plus the forced beat at close.
521
+ * Null when the operator opted out at align (D3).
522
+ */
523
+ skeleton;
396
524
  /**
397
525
  * True after a heartbeat came back 409 SESSION_NOT_ACTIVE — the session is
398
526
  * terminal server-side. The watch host uses this as its end signal when no
@@ -463,6 +591,7 @@ var SessionBridge = class {
463
591
  this.deps.redactor.text(serializeSessionEvent(full))
464
592
  );
465
593
  }
594
+ this.skeleton?.observe(full);
466
595
  if (full.kind === "assistant_message") {
467
596
  const text2 = full.payload?.text;
468
597
  if (typeof text2 === "string" && text2.trim().length > 0) {
@@ -601,7 +730,12 @@ var SessionBridge = class {
601
730
  // Sent only once a receipt has actually been observed — an empty
602
731
  // rollup would overwrite the session's totals with nulls and
603
732
  // report UNAVAILABLE for a session that had already reported.
604
- ...this.receipts.length > 0 ? { usage: this.usageRollup() } : {}
733
+ ...this.receipts.length > 0 ? { usage: this.usageRollup() } : {},
734
+ // Evidence floor (PRD §4/D2): the bounded activity skeleton rides
735
+ // the same beat (tolerant server parse — an old server strips the
736
+ // unknown key). Sent only once something was observed: null column
737
+ // means "never observed", never an empty object.
738
+ ...this.skeleton?.observedAnything ? { activitySkeleton: this.skeleton.snapshot() } : {}
605
739
  })
606
740
  }
607
741
  );
@@ -801,6 +935,7 @@ var SessionBridge = class {
801
935
  */
802
936
  async complete(opts) {
803
937
  const { pending } = await this.flushParts();
938
+ await this.flushUsageNow().catch(() => false);
804
939
  const finalResponseArtifactId = await this.pushFinalResponse();
805
940
  const rollup = this.usageRollup();
806
941
  const current = await this.deps.callTool("get_agent_session", {
@@ -1230,6 +1365,13 @@ function mapCodexRolloutLine(rawLine) {
1230
1365
  return { event: null, modelId: null, turn: null };
1231
1366
  }
1232
1367
  }
1368
+ function lastCodexRolloutModelOf(rollout) {
1369
+ let modelId = null;
1370
+ for (const rawLine of rollout.split("\n")) {
1371
+ modelId = mapCodexRolloutLine(rawLine).modelId ?? modelId;
1372
+ }
1373
+ return modelId;
1374
+ }
1233
1375
 
1234
1376
  // lib/session-codex-hooks.ts
1235
1377
  function text(value) {
@@ -1664,6 +1806,7 @@ async function runClaudeSessionHost(plan, deps = {}) {
1664
1806
  callTool: deps.callTool ?? sessionCallTool(plan.mcpUrl, bearerSource, plan.sessionId),
1665
1807
  fetchImpl: deps.fetchImpl,
1666
1808
  traceCapture,
1809
+ collectSkeleton: plan.collectSkeleton !== false,
1667
1810
  log
1668
1811
  });
1669
1812
  bridge.recordCapabilities(
@@ -1724,6 +1867,9 @@ async function runClaudeSessionHost(plan, deps = {}) {
1724
1867
  }
1725
1868
  let transcriptPath = watch ? plan.transcriptPath ?? null : null;
1726
1869
  let transcriptOffset = 0;
1870
+ let codexTurnId = null;
1871
+ let codexModelId = null;
1872
+ const codexRolloutTurnStarts = /* @__PURE__ */ new Map();
1727
1873
  const timing = new ClaudeTimingTracker();
1728
1874
  let transcriptSeen = false;
1729
1875
  let transcriptWarned = false;
@@ -1734,17 +1880,26 @@ async function runClaudeSessionHost(plan, deps = {}) {
1734
1880
  markHostTranscript(sessionDir, true, transcriptPath ?? void 0);
1735
1881
  }
1736
1882
  };
1883
+ const observePriorCodexModel = (path) => {
1884
+ if (plan.provider !== "codex" || plan.importHistory) return;
1885
+ try {
1886
+ const modelId = lastCodexRolloutModelOf(readFileSync3(path, "utf8"));
1887
+ if (modelId) {
1888
+ codexModelId = modelId;
1889
+ bridge.observeModel(modelId);
1890
+ }
1891
+ } catch {
1892
+ }
1893
+ };
1737
1894
  if (watch && transcriptPath && !plan.importHistory) {
1738
1895
  try {
1739
1896
  transcriptOffset = statSync2(transcriptPath).size;
1897
+ observePriorCodexModel(transcriptPath);
1740
1898
  noteTranscriptSeen();
1741
1899
  } catch {
1742
1900
  }
1743
1901
  }
1744
1902
  let bound = watch;
1745
- let codexTurnId = null;
1746
- let codexModelId = null;
1747
- const codexRolloutTurnStarts = /* @__PURE__ */ new Map();
1748
1903
  let sessionEnded = false;
1749
1904
  let lastPeriodicFlushAt = 0;
1750
1905
  let lastPromptAttemptAt = 0;
@@ -1841,6 +1996,7 @@ async function runClaudeSessionHost(plan, deps = {}) {
1841
1996
  transcriptPath = hookTranscript;
1842
1997
  try {
1843
1998
  transcriptOffset = plan.importHistory ? 0 : statSync2(transcriptPath).size;
1999
+ observePriorCodexModel(transcriptPath);
1844
2000
  noteTranscriptSeen();
1845
2001
  } catch {
1846
2002
  transcriptOffset = 0;
@@ -2020,9 +2176,6 @@ async function runClaudeSessionHost(plan, deps = {}) {
2020
2176
  for (const id of codexRolloutTurnStarts.keys()) {
2021
2177
  bridge.recordUnclosedInterval("turn", id);
2022
2178
  }
2023
- if (plan.provider === "codex") {
2024
- await bridge.flushUsageNow().catch(() => false);
2025
- }
2026
2179
  const end = await endRepoState(plan.repoRoot);
2027
2180
  const result = await bridge.complete({
2028
2181
  outcome: exitCode === 0 ? "COMPLETED" : "INTERRUPTED",
@@ -2066,6 +2219,7 @@ async function runCodexSessionHost(plan, deps = {}) {
2066
2219
  redactor: createSessionRedactor({ homedir: homedir() }),
2067
2220
  callTool: sessionCallTool(plan.mcpUrl, codexBearerSource, plan.sessionId),
2068
2221
  fetchImpl: deps.fetchImpl,
2222
+ collectSkeleton: plan.collectSkeleton !== false,
2069
2223
  log
2070
2224
  });
2071
2225
  bridge.recordCapabilities({
@@ -2204,4 +2358,4 @@ export {
2204
2358
  safeParse,
2205
2359
  sessionCallTool
2206
2360
  };
2207
- //# sourceMappingURL=session-host-P6W3PH3S.js.map
2361
+ //# sourceMappingURL=session-host-L6XF4T2P.js.map