@jentrix/runner 0.5.19 → 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.19\";\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-HJ2AADN6.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-NJF6CRET.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-HJ2AADN6.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", {
@@ -1152,39 +1287,90 @@ function mapCodexThreadEvent(raw) {
1152
1287
  return { event: null, unrecognized: true };
1153
1288
  }
1154
1289
  }
1290
+ var CODEX_INJECTED_USER_CONTEXT = [
1291
+ "<recommended_plugins>",
1292
+ "# AGENTS.md instructions",
1293
+ "<environment_context>"
1294
+ ];
1295
+ function codexOpeningPromptOf(rollout) {
1296
+ for (const rawLine of rollout.split("\n")) {
1297
+ try {
1298
+ const line = JSON.parse(rawLine);
1299
+ if (line.type !== "response_item" || line.payload?.type !== "message" || line.payload.role !== "user") {
1300
+ continue;
1301
+ }
1302
+ const text2 = (line.payload.content ?? []).filter((block) => block.type === "input_text").map((block) => block.text?.trim() ?? "").filter(
1303
+ (block) => block && !CODEX_INJECTED_USER_CONTEXT.some(
1304
+ (prefix) => block.startsWith(prefix)
1305
+ )
1306
+ );
1307
+ if (text2.length > 0) return text2.join("\n\n");
1308
+ } catch {
1309
+ }
1310
+ }
1311
+ return null;
1312
+ }
1155
1313
  function reportedToken(value) {
1156
1314
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
1157
1315
  }
1158
- function mapCodexRolloutUsageLine(rawLine) {
1316
+ function mapCodexRolloutLine(rawLine) {
1159
1317
  try {
1160
1318
  const line = JSON.parse(rawLine);
1319
+ const modelId = line.payload?.model?.trim() || null;
1320
+ const turnId = line.payload?.turn_id?.trim() || null;
1321
+ const at = Date.parse(line.timestamp ?? "");
1322
+ if (line.type === "turn_context") {
1323
+ return { event: null, modelId, turn: null };
1324
+ }
1325
+ if (line.type === "event_msg" && turnId && Number.isFinite(at) && (line.payload?.type === "task_started" || line.payload?.type === "task_complete")) {
1326
+ return {
1327
+ event: null,
1328
+ modelId: null,
1329
+ turn: {
1330
+ id: turnId,
1331
+ phase: line.payload.type === "task_started" ? "started" : "completed",
1332
+ at
1333
+ }
1334
+ };
1335
+ }
1161
1336
  const usage = line.payload?.info?.last_token_usage;
1162
1337
  const inputTokens = reportedToken(usage?.input_tokens);
1163
1338
  const outputTokens = reportedToken(usage?.output_tokens);
1164
1339
  if (line.type !== "event_msg" || line.payload?.type !== "token_count" || inputTokens === null || outputTokens === null) {
1165
- return null;
1340
+ return { event: null, modelId: null, turn: null };
1166
1341
  }
1167
1342
  const cacheReadTokens = reportedToken(usage?.cached_input_tokens);
1168
1343
  const cacheCreationTokens = reportedToken(usage?.cache_write_input_tokens);
1169
1344
  const reasoningOutputTokens = reportedToken(usage?.reasoning_output_tokens);
1170
1345
  return {
1171
- ...make(
1172
- "usage",
1173
- {
1174
- kind: "delta",
1175
- inputTokens,
1176
- outputTokens,
1177
- ...cacheReadTokens !== null ? { cacheReadTokens } : {},
1178
- ...cacheCreationTokens !== null ? { cacheCreationTokens } : {},
1179
- ...reasoningOutputTokens !== null ? { reasoningOutputTokens } : {}
1180
- },
1181
- typeof line.ordinal === "number" ? `rollout:${line.ordinal}` : line.timestamp
1182
- ),
1183
- ...line.timestamp ? { at: line.timestamp } : {}
1346
+ event: {
1347
+ ...make(
1348
+ "usage",
1349
+ {
1350
+ kind: "delta",
1351
+ inputTokens,
1352
+ outputTokens,
1353
+ ...cacheReadTokens !== null ? { cacheReadTokens } : {},
1354
+ ...cacheCreationTokens !== null ? { cacheCreationTokens } : {},
1355
+ ...reasoningOutputTokens !== null ? { reasoningOutputTokens } : {}
1356
+ },
1357
+ typeof line.ordinal === "number" ? `rollout:${line.ordinal}` : line.timestamp
1358
+ ),
1359
+ ...line.timestamp ? { at: line.timestamp } : {}
1360
+ },
1361
+ modelId: null,
1362
+ turn: null
1184
1363
  };
1185
1364
  } catch {
1186
- return null;
1365
+ return { event: null, modelId: null, turn: null };
1366
+ }
1367
+ }
1368
+ function lastCodexRolloutModelOf(rollout) {
1369
+ let modelId = null;
1370
+ for (const rawLine of rollout.split("\n")) {
1371
+ modelId = mapCodexRolloutLine(rawLine).modelId ?? modelId;
1187
1372
  }
1373
+ return modelId;
1188
1374
  }
1189
1375
 
1190
1376
  // lib/session-codex-hooks.ts
@@ -1620,6 +1806,7 @@ async function runClaudeSessionHost(plan, deps = {}) {
1620
1806
  callTool: deps.callTool ?? sessionCallTool(plan.mcpUrl, bearerSource, plan.sessionId),
1621
1807
  fetchImpl: deps.fetchImpl,
1622
1808
  traceCapture,
1809
+ collectSkeleton: plan.collectSkeleton !== false,
1623
1810
  log
1624
1811
  });
1625
1812
  bridge.recordCapabilities(
@@ -1680,6 +1867,9 @@ async function runClaudeSessionHost(plan, deps = {}) {
1680
1867
  }
1681
1868
  let transcriptPath = watch ? plan.transcriptPath ?? null : null;
1682
1869
  let transcriptOffset = 0;
1870
+ let codexTurnId = null;
1871
+ let codexModelId = null;
1872
+ const codexRolloutTurnStarts = /* @__PURE__ */ new Map();
1683
1873
  const timing = new ClaudeTimingTracker();
1684
1874
  let transcriptSeen = false;
1685
1875
  let transcriptWarned = false;
@@ -1690,16 +1880,26 @@ async function runClaudeSessionHost(plan, deps = {}) {
1690
1880
  markHostTranscript(sessionDir, true, transcriptPath ?? void 0);
1691
1881
  }
1692
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
+ };
1693
1894
  if (watch && transcriptPath && !plan.importHistory) {
1694
1895
  try {
1695
1896
  transcriptOffset = statSync2(transcriptPath).size;
1897
+ observePriorCodexModel(transcriptPath);
1696
1898
  noteTranscriptSeen();
1697
1899
  } catch {
1698
1900
  }
1699
1901
  }
1700
1902
  let bound = watch;
1701
- let codexTurnId = null;
1702
- let codexModelId = null;
1703
1903
  let sessionEnded = false;
1704
1904
  let lastPeriodicFlushAt = 0;
1705
1905
  let lastPromptAttemptAt = 0;
@@ -1715,13 +1915,14 @@ async function runClaudeSessionHost(plan, deps = {}) {
1715
1915
  }
1716
1916
  return;
1717
1917
  }
1718
- if (!transcriptPath || plan.provider !== "claude") return;
1918
+ if (!transcriptPath) return;
1719
1919
  const nowMs = Date.now();
1720
1920
  if (nowMs - lastPromptAttemptAt < 3e4) return;
1721
1921
  lastPromptAttemptAt = nowMs;
1722
1922
  let prompt = null;
1723
1923
  try {
1724
- prompt = openingPromptOf(readFileSync3(transcriptPath, "utf8"));
1924
+ const transcript = readFileSync3(transcriptPath, "utf8");
1925
+ prompt = plan.provider === "claude" ? openingPromptOf(transcript) : codexOpeningPromptOf(transcript);
1725
1926
  } catch {
1726
1927
  return;
1727
1928
  }
@@ -1795,6 +1996,7 @@ async function runClaudeSessionHost(plan, deps = {}) {
1795
1996
  transcriptPath = hookTranscript;
1796
1997
  try {
1797
1998
  transcriptOffset = plan.importHistory ? 0 : statSync2(transcriptPath).size;
1999
+ observePriorCodexModel(transcriptPath);
1798
2000
  noteTranscriptSeen();
1799
2001
  } catch {
1800
2002
  transcriptOffset = 0;
@@ -1876,7 +2078,32 @@ async function runClaudeSessionHost(plan, deps = {}) {
1876
2078
  transcriptOffset = tail.offset;
1877
2079
  if (tail.body) {
1878
2080
  for (const rawLine of tail.body.split("\n").filter(Boolean)) {
1879
- const event = mapCodexRolloutUsageLine(rawLine);
2081
+ const mapped = mapCodexRolloutLine(rawLine);
2082
+ if (mapped.modelId) {
2083
+ codexModelId = mapped.modelId;
2084
+ bridge.observeModel(mapped.modelId);
2085
+ }
2086
+ if (mapped.turn?.phase === "started") {
2087
+ codexTurnId = mapped.turn.id;
2088
+ codexRolloutTurnStarts.set(mapped.turn.id, mapped.turn.at);
2089
+ } else if (mapped.turn?.phase === "completed") {
2090
+ const startedAt = codexRolloutTurnStarts.get(mapped.turn.id);
2091
+ if (startedAt !== void 0) {
2092
+ if (mapped.turn.at >= startedAt) {
2093
+ bridge.recordInterval({
2094
+ kind: "turn",
2095
+ id: mapped.turn.id,
2096
+ startedAt,
2097
+ endedAt: mapped.turn.at
2098
+ });
2099
+ } else {
2100
+ bridge.recordUnclosedInterval("turn", mapped.turn.id);
2101
+ }
2102
+ codexRolloutTurnStarts.delete(mapped.turn.id);
2103
+ }
2104
+ if (codexTurnId === mapped.turn.id) codexTurnId = null;
2105
+ }
2106
+ const event = mapped.event;
1880
2107
  if (!event) continue;
1881
2108
  bridge.record({
1882
2109
  ...event,
@@ -1946,6 +2173,9 @@ async function runClaudeSessionHost(plan, deps = {}) {
1946
2173
  for (const id of timing.unclosedToolIds()) {
1947
2174
  bridge.recordUnclosedInterval("tool", id);
1948
2175
  }
2176
+ for (const id of codexRolloutTurnStarts.keys()) {
2177
+ bridge.recordUnclosedInterval("turn", id);
2178
+ }
1949
2179
  const end = await endRepoState(plan.repoRoot);
1950
2180
  const result = await bridge.complete({
1951
2181
  outcome: exitCode === 0 ? "COMPLETED" : "INTERRUPTED",
@@ -1989,6 +2219,7 @@ async function runCodexSessionHost(plan, deps = {}) {
1989
2219
  redactor: createSessionRedactor({ homedir: homedir() }),
1990
2220
  callTool: sessionCallTool(plan.mcpUrl, codexBearerSource, plan.sessionId),
1991
2221
  fetchImpl: deps.fetchImpl,
2222
+ collectSkeleton: plan.collectSkeleton !== false,
1992
2223
  log
1993
2224
  });
1994
2225
  bridge.recordCapabilities({
@@ -2127,4 +2358,4 @@ export {
2127
2358
  safeParse,
2128
2359
  sessionCallTool
2129
2360
  };
2130
- //# sourceMappingURL=session-host-NJF6CRET.js.map
2361
+ //# sourceMappingURL=session-host-L6XF4T2P.js.map