@manyoars/oars 0.4.1 → 0.5.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.
- package/README.md +79 -29
- package/dist/bin.js +3 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +206 -20
- package/dist/club.d.ts +19 -0
- package/dist/club.js +132 -0
- package/dist/gate/hook-fill-main.js +1 -1
- package/dist/gate/hook-fill.d.ts +1 -1
- package/dist/gate/hook-fill.js +1 -1
- package/dist/gate/hook-main.js +7 -5
- package/dist/gate/hook.d.ts +12 -1
- package/dist/gate/hook.js +38 -7
- package/dist/hook-bin.js +23 -4
- package/dist/main.js +5 -0
- package/dist/node-floor.d.ts +10 -0
- package/dist/node-floor.js +23 -0
- package/dist/oars-hook.d.ts +2 -0
- package/dist/oars-hook.js +7 -0
- package/dist/sessions/allow.d.ts +1 -0
- package/dist/sessions/allow.js +1 -1
- package/dist/sessions/call-shape.d.ts +111 -0
- package/dist/sessions/call-shape.js +464 -0
- package/dist/sessions/candidates.d.ts +65 -0
- package/dist/sessions/candidates.js +236 -0
- package/dist/sessions/card-command.d.ts +24 -0
- package/dist/sessions/card-command.js +71 -0
- package/dist/sessions/card-font.d.ts +6 -0
- package/dist/sessions/card-font.js +36 -0
- package/dist/sessions/card-line.d.ts +30 -0
- package/dist/sessions/card-line.js +105 -0
- package/dist/sessions/card-render.d.ts +15 -0
- package/dist/sessions/card-render.js +146 -0
- package/dist/sessions/card.d.ts +59 -0
- package/dist/sessions/card.js +107 -0
- package/dist/sessions/cell-increments.d.ts +14 -3
- package/dist/sessions/cell-increments.js +79 -12
- package/dist/sessions/cells.d.ts +42 -4
- package/dist/sessions/cells.js +72 -18
- package/dist/sessions/codex-hook-bin.d.ts +1 -0
- package/dist/sessions/codex-hook-bin.js +24 -0
- package/dist/sessions/codex-hook-install.d.ts +21 -0
- package/dist/sessions/codex-hook-install.js +47 -0
- package/dist/sessions/codex-hook-view.d.ts +20 -0
- package/dist/sessions/codex-hook-view.js +20 -0
- package/dist/sessions/codex-hook.d.ts +77 -0
- package/dist/sessions/codex-hook.js +255 -0
- package/dist/sessions/codex.d.ts +9 -0
- package/dist/sessions/codex.js +104 -18
- package/dist/sessions/command.d.ts +17 -2
- package/dist/sessions/command.js +145 -33
- package/dist/sessions/copilot.d.ts +3 -0
- package/dist/sessions/copilot.js +12 -4
- package/dist/sessions/derive.js +2 -1
- package/dist/sessions/empty-state.d.ts +17 -0
- package/dist/sessions/empty-state.js +38 -0
- package/dist/sessions/exclusion.js +48 -3
- package/dist/sessions/extract.d.ts +16 -3
- package/dist/sessions/extract.js +281 -24
- package/dist/sessions/gemini.d.ts +2 -0
- package/dist/sessions/gemini.js +29 -2
- package/dist/sessions/harness-files.d.ts +27 -0
- package/dist/sessions/harness-files.js +109 -0
- package/dist/sessions/head-only.d.ts +7 -0
- package/dist/sessions/head-only.js +21 -0
- package/dist/sessions/hook-calls.js +4 -3
- package/dist/sessions/hooks.d.ts +1 -1
- package/dist/sessions/hooks.js +6 -2
- package/dist/sessions/identity.d.ts +6 -0
- package/dist/sessions/injected.d.ts +13 -0
- package/dist/sessions/injected.js +212 -0
- package/dist/sessions/install.d.ts +31 -20
- package/dist/sessions/install.js +80 -82
- package/dist/sessions/load-status.d.ts +41 -0
- package/dist/sessions/load-status.js +156 -0
- package/dist/sessions/machine-child-deps.d.ts +5 -0
- package/dist/sessions/machine-child-deps.js +83 -0
- package/dist/sessions/machine-child.d.ts +43 -0
- package/dist/sessions/machine-child.js +120 -0
- package/dist/sessions/machine-credential.d.ts +10 -0
- package/dist/sessions/machine-credential.js +20 -0
- package/dist/sessions/machine-enroll.d.ts +47 -0
- package/dist/sessions/machine-enroll.js +230 -0
- package/dist/sessions/machine-hold.d.ts +49 -0
- package/dist/sessions/machine-hold.js +88 -0
- package/dist/sessions/machine-ids.d.ts +6 -0
- package/dist/sessions/machine-ids.js +9 -0
- package/dist/sessions/machine-notice.d.ts +21 -0
- package/dist/sessions/machine-notice.js +89 -0
- package/dist/sessions/machine-pull.d.ts +40 -0
- package/dist/sessions/machine-pull.js +220 -0
- package/dist/sessions/machine-repo.d.ts +7 -0
- package/dist/sessions/machine-repo.js +30 -0
- package/dist/sessions/machine-scan-cache.d.ts +12 -0
- package/dist/sessions/machine-scan-cache.js +47 -0
- package/dist/sessions/machine-send.d.ts +12 -0
- package/dist/sessions/machine-send.js +156 -0
- package/dist/sessions/machine-stop.d.ts +22 -0
- package/dist/sessions/machine-stop.js +53 -0
- package/dist/sessions/machine.d.ts +41 -0
- package/dist/sessions/machine.js +89 -0
- package/dist/sessions/opencode.d.ts +2 -0
- package/dist/sessions/opencode.js +6 -0
- package/dist/sessions/otel.d.ts +21 -0
- package/dist/sessions/otel.js +109 -38
- package/dist/sessions/owner-only.d.ts +7 -0
- package/dist/sessions/owner-only.js +34 -0
- package/dist/sessions/pi.d.ts +13 -1
- package/dist/sessions/pi.js +27 -13
- package/dist/sessions/pool-books.d.ts +1 -1
- package/dist/sessions/pool-books.js +1 -1
- package/dist/sessions/pool-choices.d.ts +4 -2
- package/dist/sessions/pool-choices.js +4 -2
- package/dist/sessions/pool-command.d.ts +46 -6
- package/dist/sessions/pool-command.js +577 -133
- package/dist/sessions/pool-delta.d.ts +7 -0
- package/dist/sessions/pool-delta.js +28 -5
- package/dist/sessions/pool-due.d.ts +43 -0
- package/dist/sessions/pool-due.js +113 -0
- package/dist/sessions/pool-nightly.d.ts +9 -1
- package/dist/sessions/pool-nightly.js +65 -21
- package/dist/sessions/pool-outbox.d.ts +51 -5
- package/dist/sessions/pool-outbox.js +217 -24
- package/dist/sessions/pool-print.d.ts +1 -1
- package/dist/sessions/pool-raw.js +4 -3
- package/dist/sessions/pool-replica.d.ts +4 -4
- package/dist/sessions/pool-replica.js +26 -7
- package/dist/sessions/pool-send-lock.js +1 -1
- package/dist/sessions/pool-sent.d.ts +2 -0
- package/dist/sessions/pool-sent.js +21 -4
- package/dist/sessions/pool-wire.d.ts +172 -12
- package/dist/sessions/pool-wire.js +284 -30
- package/dist/sessions/pool.js +4 -2
- package/dist/sessions/public-vocabulary.d.ts +5 -0
- package/dist/sessions/public-vocabulary.js +16 -0
- package/dist/sessions/read-call-shape.js +12 -6
- package/dist/sessions/read-fit.d.ts +5 -0
- package/dist/sessions/read-fit.js +23 -2
- package/dist/sessions/read.d.ts +1 -1
- package/dist/sessions/recall-consequence.d.ts +2 -2
- package/dist/sessions/recall-consequence.js +14 -9
- package/dist/sessions/recall-object.d.ts +9 -2
- package/dist/sessions/receipt.d.ts +8 -0
- package/dist/sessions/receipt.js +78 -0
- package/dist/sessions/recovery-calls.d.ts +3 -2
- package/dist/sessions/recovery-calls.js +9 -3
- package/dist/sessions/redact.d.ts +2 -2
- package/dist/sessions/redact.js +116 -14
- package/dist/sessions/reversion.d.ts +8 -0
- package/dist/sessions/reversion.js +114 -0
- package/dist/sessions/schema.d.ts +78 -4
- package/dist/sessions/schema.js +31 -1
- package/dist/sessions/session-shape.d.ts +2 -1
- package/dist/sessions/session-shape.js +15 -0
- package/dist/sessions/sources.d.ts +9 -0
- package/dist/sessions/sources.js +16 -5
- package/dist/sessions/steps.js +3 -2
- package/dist/sessions/store.d.ts +41 -2
- package/dist/sessions/store.js +68 -16
- package/dist/sessions/try-demo.d.ts +8 -0
- package/dist/sessions/try-demo.js +90 -0
- package/dist/sessions/try.d.ts +13 -0
- package/dist/sessions/try.js +50 -0
- package/dist/sessions/vscode-copilot.d.ts +4 -0
- package/dist/sessions/vscode-copilot.js +25 -18
- package/dist/telemetry/command.js +12 -6
- package/dist/telemetry/consent.d.ts +9 -3
- package/dist/telemetry/consent.js +25 -9
- package/dist/telemetry/fields.d.ts +11 -0
- package/dist/telemetry/fields.js +56 -0
- package/dist/telemetry/measures.d.ts +13 -0
- package/dist/telemetry/measures.js +30 -0
- package/dist/telemetry/observe.d.ts +15 -2
- package/dist/telemetry/observe.js +34 -7
- package/dist/telemetry/region.d.ts +6 -0
- package/dist/telemetry/region.js +28 -0
- package/dist/telemetry/report.js +1 -1
- package/dist/telemetry/schema.d.ts +3 -0
- package/dist/telemetry/schema.js +11 -1
- package/dist/telemetry/send.d.ts +5 -2
- package/dist/telemetry/send.js +15 -7
- package/dist/telemetry/words.d.ts +6 -0
- package/dist/telemetry/words.js +6 -0
- package/package.json +23 -4
package/dist/sessions/codex.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface CodexStore {
|
|
|
36
36
|
sessions: CodexSession[];
|
|
37
37
|
/** The session as transcript text for `captureSession`; throws when the file cannot be read. */
|
|
38
38
|
transcript: (session: CodexSession) => Promise<string>;
|
|
39
|
+
/** True when the rollout holds only head records (`rolloutHeadOnly`); `sessions verify` counts such a file as empty, not unreadable. */
|
|
40
|
+
headOnly: (session: CodexSession) => Promise<boolean>;
|
|
39
41
|
/** Files skipped because their id is not a safe file name (it names a skeleton), counted so the skip is never silent. */
|
|
40
42
|
rejected: number;
|
|
41
43
|
/** Files that could not be inspected, reported once each and never fatal; the reason names no path, so a log line built from it leaks none. */
|
|
@@ -46,6 +48,13 @@ export interface CodexStore {
|
|
|
46
48
|
}
|
|
47
49
|
/** The non-blank lines of a rollout that are not JSON (a torn write), kept so `captureSession` counts them and the deposit line reports them. */
|
|
48
50
|
export declare const rolloutBadLines: (text: string) => string[];
|
|
51
|
+
export declare const rolloutHeadOnly: (text: string) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The rollout with the history it inherited removed (the records below a subagent's `subagent_history_start_ordinal`, and a Copied fork's leading
|
|
54
|
+
* copy of its parent), by the same two rules `rolloutLines` applies, so the head-only test sees only what the session itself wrote. Lines that are not
|
|
55
|
+
* a JSON object with a payload stay, as a torn line must still count.
|
|
56
|
+
*/
|
|
57
|
+
export declare const rolloutOwnText: (text: string, parentText?: string) => string;
|
|
49
58
|
/** One rollout as the lines `captureSession` reads. */
|
|
50
59
|
export declare const rolloutLines: (text: string, fallbackId: string, parentText?: string) => Obj[];
|
|
51
60
|
/** Opens the Codex store under `root`; undefined when it holds no `sessions` folder, so a machine without Codex is skipped silently. */
|
package/dist/sessions/codex.js
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
// That these records are persisted into the rollout is rollout/src/policy.rs:83, :91, and :137 at ac558e2.
|
|
24
24
|
// event_msg user_message {message} a prompt (the `response_item` user messages also hold injected context, so they are not read)
|
|
25
25
|
// event_msg agent_message {message} the assistant's text
|
|
26
|
-
// event_msg token_count {info.last_token_usage} one model response's usage, closing the lines since the last one
|
|
26
|
+
// event_msg token_count {info.last_token_usage, rate_limits.primary/.secondary {used_percent, window_minutes}} one model response's usage, closing the lines since the last one, and the plan share used when the event holds it
|
|
27
|
+
// (read in preference to `token_usage_record`: it is the event already read for usage, it is the one that carries `rate_limits`, and it is on more responses)
|
|
27
28
|
// response_item function_call / local_shell_call / custom_tool_call a tool call
|
|
28
29
|
// response_item function_call_output {call_id, output} a shell call's outcome: the text `Exit code: N` / `Process exited with code N` then `Output:`
|
|
29
30
|
// (the unified-exec emitter's own text, core/src/tools/events.rs:400, :416, :423; context.rs:556), the real exit code; `execution error: ...` (core/src/tools/events.rs:438) is a
|
|
@@ -53,6 +54,7 @@ import { basename, join } from 'node:path';
|
|
|
53
54
|
import { createInterface } from 'node:readline';
|
|
54
55
|
import { PassThrough, pipeline } from 'node:stream';
|
|
55
56
|
import { createZstdDecompress, zstdDecompressSync } from 'node:zlib';
|
|
57
|
+
import { holdsOnlyRecords } from './head-only.js';
|
|
56
58
|
import { INTERRUPT_MARKER, SHAPE_TOOLS_LINE } from './session-shape.js';
|
|
57
59
|
/** The Codex home for an environment; a test passes its own so it never reads the real one. */
|
|
58
60
|
export const codexRoot = (env, home = homedir()) => {
|
|
@@ -230,30 +232,68 @@ const outcomeOf = (raw) => {
|
|
|
230
232
|
* an `Exit code -1` line would be the error line and the message lost. The message stands alone then, and a code is the line capture skips.
|
|
231
233
|
*/
|
|
232
234
|
const failureOf = (exit, text) => exit === undefined || exit === 0 ? undefined : exit < 0 && text.trim() !== '' ? text : `Exit code ${String(exit)}\n${text}`;
|
|
235
|
+
/** One `rate_limits` window: the plan share used and the window's length. Absent when the event holds no number for it, never a zero. */
|
|
236
|
+
const windowOf = (raw) => {
|
|
237
|
+
if (!isObj(raw))
|
|
238
|
+
return undefined;
|
|
239
|
+
const usedPercent = num(raw.used_percent);
|
|
240
|
+
if (usedPercent === undefined)
|
|
241
|
+
return undefined;
|
|
242
|
+
const windowMinutes = num(raw.window_minutes);
|
|
243
|
+
return { usedPercent, ...(windowMinutes === undefined ? {} : { windowMinutes }) };
|
|
244
|
+
};
|
|
245
|
+
/** The event's `rate_limits.primary` and `.secondary`, or undefined when it holds neither (older rollouts and local models write none). */
|
|
246
|
+
const rateLimitsOf = (payload) => {
|
|
247
|
+
const limits = isObj(payload.rate_limits) ? payload.rate_limits : undefined;
|
|
248
|
+
const primary = windowOf(limits?.primary);
|
|
249
|
+
const secondary = windowOf(limits?.secondary);
|
|
250
|
+
return primary === undefined && secondary === undefined ? undefined : { ...(primary === undefined ? {} : { primary }), ...(secondary === undefined ? {} : { secondary }) };
|
|
251
|
+
};
|
|
233
252
|
const usageOf = (info) => {
|
|
234
253
|
const last = isObj(info) && isObj(info.last_token_usage) ? info.last_token_usage : undefined;
|
|
235
254
|
if (last === undefined)
|
|
236
255
|
return undefined;
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
256
|
+
const reportedInput = num(last.input_tokens);
|
|
257
|
+
const input = reportedInput ?? 0;
|
|
258
|
+
const reportedCached = num(last.cached_input_tokens);
|
|
259
|
+
const cached = Math.min(reportedCached ?? 0, input);
|
|
260
|
+
const reportedOutput = num(last.output_tokens);
|
|
261
|
+
const output = reportedOutput ?? 0;
|
|
240
262
|
// A zero total means Codex recorded none, so no usage is written rather than a measured zero.
|
|
241
263
|
if (input === 0 && output === 0)
|
|
242
264
|
return undefined;
|
|
243
265
|
const reasoning = num(last.reasoning_output_tokens);
|
|
244
|
-
const
|
|
266
|
+
const reportedWritten = num(last.cache_write_input_tokens);
|
|
267
|
+
const written = reportedWritten ?? 0;
|
|
245
268
|
// Codex counts cached and written tokens both inside `input_tokens` (input 100 = cached 40 + write 60, codex-api/src/sse/responses.rs:889-911); capture's
|
|
246
269
|
// `input` excludes cache reads and creation, as the Claude Code path's does, so each token is counted once.
|
|
247
270
|
return {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
271
|
+
// A category the event left out stays out (a partial event, or an older rollout with no cache keys), so it is neither a zero entry nor an observed field.
|
|
272
|
+
...(reportedInput === undefined ? {} : { input_tokens: Math.max(input - cached - written, 0) }),
|
|
273
|
+
...(reportedCached === undefined ? {} : { cache_read_input_tokens: cached }),
|
|
274
|
+
...(reportedWritten === undefined ? {} : { cache_creation_input_tokens: written }),
|
|
275
|
+
...(reportedOutput === undefined ? {} : { output_tokens: output }),
|
|
252
276
|
...(reasoning === undefined ? {} : { output_tokens_details: { thinking_tokens: reasoning } }),
|
|
253
277
|
};
|
|
254
278
|
};
|
|
255
279
|
/** The non-blank lines of a rollout that are not JSON (a torn write), kept so `captureSession` counts them and the deposit line reports them. */
|
|
256
280
|
export const rolloutBadLines = (text) => text.split('\n').filter((raw) => raw.trim() !== '' && parse(raw) === undefined);
|
|
281
|
+
/** Rollout record types that carry no turn by design: the header and the per-turn context. A rollout of these alone is a session started and quit. */
|
|
282
|
+
const ROLLOUT_HEAD_KINDS = new Set(['session_meta', 'turn_context']);
|
|
283
|
+
/** Payload types the reader reads as carrying no turn: the usage event, an aborted turn (one the operator interrupted gives a prompt record, so it is no longer head-only), and the `response_item` messages of injected context (a prompt is the `event_msg` user_message). */
|
|
284
|
+
const ROLLOUT_HEAD_EVENTS = new Set(['token_count', 'turn_aborted']);
|
|
285
|
+
const ROLLOUT_HEAD_ITEMS = new Set(['message']);
|
|
286
|
+
/** An assistant `response_item` message is a turn, which the reader reads from `event_msg` only: a rollout whose turns moved to it is a moved form, not an empty one. */
|
|
287
|
+
const isAssistantItem = (record) => typeof record.payload === 'object' && record.payload !== null && 'role' in record.payload && record.payload.role === 'assistant';
|
|
288
|
+
export const rolloutHeadOnly = (text) => holdsOnlyRecords(text, (record) => {
|
|
289
|
+
if (ROLLOUT_HEAD_KINDS.has(record.type))
|
|
290
|
+
return true;
|
|
291
|
+
const payload = record.payload;
|
|
292
|
+
const kind = typeof payload === 'object' && payload !== null && 'type' in payload ? payload.type : undefined;
|
|
293
|
+
if (typeof kind !== 'string')
|
|
294
|
+
return false;
|
|
295
|
+
return (record.type === 'event_msg' && ROLLOUT_HEAD_EVENTS.has(kind)) || (record.type === 'response_item' && ROLLOUT_HEAD_ITEMS.has(kind) && !isAssistantItem(record));
|
|
296
|
+
});
|
|
257
297
|
/**
|
|
258
298
|
* The names in a list of tool declarations: each entry's `name` (a `DynamicToolSpec` function, dynamic_tools.rs; an `additional_tools` or
|
|
259
299
|
* `tool_search_output` entry is an untyped value, models.rs:1076, :1257), and the names of a namespace's own `tools`; a namespace's own name is a grouping, not a tool. Nothing else is read: a
|
|
@@ -264,6 +304,49 @@ const toolNames = (list) => (Array.isArray(list) ? list : [])
|
|
|
264
304
|
.filter((name) => name !== undefined && name !== '');
|
|
265
305
|
/** The approval policy as the permission mode: a string, or the tag of an externally tagged object (`{granular: {...}}`, protocol.rs:1181), which `mode()` keeps if documented and stores as `other` if not. */
|
|
266
306
|
const approvalMode = (policy) => (isObj(policy) ? Object.keys(policy)[0] : str(policy));
|
|
307
|
+
/** The parent's records (not its `session_meta`) as the keys a copied fork's leading records are matched against. */
|
|
308
|
+
const parentKeys = (parent) => parent
|
|
309
|
+
.split('\n')
|
|
310
|
+
.map((raw) => parse(raw))
|
|
311
|
+
.filter((parsed) => isObj(parsed) && isObj(parsed.payload) && parsed.type !== 'session_meta')
|
|
312
|
+
.map((parsed) => JSON.stringify([parsed.type, parsed.payload]));
|
|
313
|
+
/** True for a `session_meta` of a Copied fork whose parent text is known: the leading records equal to the parent's are its copied history, in either mode. */
|
|
314
|
+
const copiesParent = (payload, parentText) => parentText !== undefined && str(payload.forked_from_id) !== undefined && !isObj(payload.history_base);
|
|
315
|
+
/**
|
|
316
|
+
* The rollout with the history it inherited removed (the records below a subagent's `subagent_history_start_ordinal`, and a Copied fork's leading
|
|
317
|
+
* copy of its parent), by the same two rules `rolloutLines` applies, so the head-only test sees only what the session itself wrote. Lines that are not
|
|
318
|
+
* a JSON object with a payload stay, as a torn line must still count.
|
|
319
|
+
*/
|
|
320
|
+
export const rolloutOwnText = (text, parentText) => {
|
|
321
|
+
let inherited;
|
|
322
|
+
let copied = null;
|
|
323
|
+
let seenMeta = false;
|
|
324
|
+
return text
|
|
325
|
+
.split('\n')
|
|
326
|
+
.filter((raw) => {
|
|
327
|
+
const entry = parse(raw);
|
|
328
|
+
if (!isObj(entry) || !isObj(entry.payload))
|
|
329
|
+
return true;
|
|
330
|
+
const ordinal = num(entry.ordinal);
|
|
331
|
+
if (inherited !== undefined && ordinal !== undefined && ordinal < inherited)
|
|
332
|
+
return false;
|
|
333
|
+
if (entry.type !== 'session_meta' && copied !== null) {
|
|
334
|
+
if (copied.keys[copied.at] === JSON.stringify([entry.type, entry.payload])) {
|
|
335
|
+
copied.at += 1;
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
copied = null;
|
|
339
|
+
}
|
|
340
|
+
if (entry.type === 'session_meta' && !seenMeta) {
|
|
341
|
+
seenMeta = true;
|
|
342
|
+
inherited = num(entry.payload.subagent_history_start_ordinal);
|
|
343
|
+
if (inherited === undefined && copiesParent(entry.payload, parentText))
|
|
344
|
+
copied = { keys: parentKeys(parentText), at: 0 };
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
})
|
|
348
|
+
.join('\n');
|
|
349
|
+
};
|
|
267
350
|
/** One rollout as the lines `captureSession` reads. */
|
|
268
351
|
export const rolloutLines = (text, fallbackId, parentText) => {
|
|
269
352
|
const nonblank = text.split('\n').filter((raw) => raw.trim() !== '');
|
|
@@ -439,11 +522,6 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
439
522
|
settleMcp(id, failed ? (text === '' ? 'dynamic tool call failed' : text) : undefined, when);
|
|
440
523
|
}
|
|
441
524
|
};
|
|
442
|
-
const parentKeys = (parent) => parent
|
|
443
|
-
.split('\n')
|
|
444
|
-
.map((raw) => parse(raw))
|
|
445
|
-
.filter((parsed) => isObj(parsed) && isObj(parsed.payload) && parsed.type !== 'session_meta')
|
|
446
|
-
.map((parsed) => JSON.stringify([parsed.type, parsed.payload]));
|
|
447
525
|
for (const raw of text.split('\n')) {
|
|
448
526
|
const entry = parse(raw);
|
|
449
527
|
if (!isObj(entry) || !isObj(entry.payload))
|
|
@@ -482,7 +560,7 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
482
560
|
dynamicNames.add(name);
|
|
483
561
|
inherited = num(payload.subagent_history_start_ordinal);
|
|
484
562
|
paginated = payload.history_mode === 'paginated';
|
|
485
|
-
if (inherited === undefined &&
|
|
563
|
+
if (inherited === undefined && copiesParent(payload, parentText))
|
|
486
564
|
copied = { keys: parentKeys(parentText), at: 0 };
|
|
487
565
|
}
|
|
488
566
|
else if (type === 'turn_context') {
|
|
@@ -527,6 +605,10 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
527
605
|
const first = open.message;
|
|
528
606
|
if (isObj(first))
|
|
529
607
|
first.usage = usage;
|
|
608
|
+
// The plan reading rides the response's first line beside its usage, so a repeated event adds neither.
|
|
609
|
+
const rateLimits = rateLimitsOf(payload);
|
|
610
|
+
if (rateLimits !== undefined)
|
|
611
|
+
open.rateLimits = rateLimits;
|
|
530
612
|
lastTotal = total;
|
|
531
613
|
response += 1;
|
|
532
614
|
open = undefined;
|
|
@@ -727,6 +809,11 @@ export const openCodexStore = async (root) => {
|
|
|
727
809
|
origin.mtimeMs = Math.max(origin.mtimeMs, own.mtimeMs);
|
|
728
810
|
sessions.delete(id);
|
|
729
811
|
}
|
|
812
|
+
// A Copied fork's history is found by matching it against the parent's lineage as of the fork's start; a parent outside the store (or unreadable) leaves it whole.
|
|
813
|
+
const parentTextOf = async (session) => {
|
|
814
|
+
const parent = session.forkedFrom === null ? undefined : sessions.get(session.forkedFrom);
|
|
815
|
+
return parent === undefined ? undefined : lineageText(parent, session.started).catch(() => undefined);
|
|
816
|
+
};
|
|
730
817
|
const ordered = [...sessions.values()].sort((a, b) => a.mtimeMs - b.mtimeMs || a.id.localeCompare(b.id));
|
|
731
818
|
return {
|
|
732
819
|
sessions: ordered,
|
|
@@ -735,11 +822,10 @@ export const openCodexStore = async (root) => {
|
|
|
735
822
|
transcript: async (session) => {
|
|
736
823
|
// A rollout that vanished after it was listed (Codex compresses an old one) fails with its reason, which names no path.
|
|
737
824
|
const text = await lineageText(session);
|
|
738
|
-
|
|
739
|
-
const parent = session.forkedFrom === null ? undefined : sessions.get(session.forkedFrom);
|
|
740
|
-
const parentText = parent === undefined ? undefined : await lineageText(parent, session.started).catch(() => undefined);
|
|
825
|
+
const parentText = await parentTextOf(session);
|
|
741
826
|
const lines = [...rolloutLines(text, session.id, parentText).map((line) => JSON.stringify(line)), ...rolloutBadLines(text)];
|
|
742
827
|
return lines.join('\n') + (lines.length === 0 ? '' : '\n');
|
|
743
828
|
},
|
|
829
|
+
headOnly: async (session) => rolloutHeadOnly(rolloutOwnText(await lineageText(session), await parentTextOf(session))),
|
|
744
830
|
};
|
|
745
831
|
};
|
|
@@ -4,6 +4,7 @@ import type { Emit } from '../telemetry/emit.js';
|
|
|
4
4
|
import { type PullInput } from './recall-reads.js';
|
|
5
5
|
import { type ContextOptions } from './hook-index.js';
|
|
6
6
|
import { type InstallRuntime, type Scope } from './install.js';
|
|
7
|
+
import { type HarnessFiles } from './harness-files.js';
|
|
7
8
|
import { type SessionsRuntime, type SkippedScope } from './store.js';
|
|
8
9
|
import { type StuckStep } from './steps.js';
|
|
9
10
|
import { type SelfBaselineResult } from './baseline.js';
|
|
@@ -27,6 +28,11 @@ export declare const sessionsDeposit: (deps: SessionsDeps, options: {
|
|
|
27
28
|
* this command answers an operator, so the `--error`, `--path`, and `--class` forms keep the signature text they have always printed; the MCP tool, which answers an agent, does not.
|
|
28
29
|
*/
|
|
29
30
|
export declare const sessionsRecall: (deps: SessionsDeps, query: PullInput, sid?: string | null) => Promise<Outcome>;
|
|
31
|
+
/**
|
|
32
|
+
* `recall --error <text> --receipt`: the same pull, recorded as any recall is, printed as a paste-ready block for an issue. Only `--error` asks about one error;
|
|
33
|
+
* every other form refuses, because a receipt states one signature.
|
|
34
|
+
*/
|
|
35
|
+
export declare const sessionsReceipt: (deps: SessionsDeps, query: PullInput, sid?: string | null) => Promise<Outcome>;
|
|
30
36
|
/** `recall --counts`: push and pull, shown and held, as counts only. */
|
|
31
37
|
export declare const sessionsRecallCounts: (deps: SessionsDeps, day: string | undefined) => Promise<Outcome>;
|
|
32
38
|
/** `lineage`: a walk over files the derive already wrote. It computes nothing, so it logs no step. */
|
|
@@ -69,6 +75,8 @@ export interface VerifyReport {
|
|
|
69
75
|
scope: SkippedScope;
|
|
70
76
|
/** Every session's rows 1 to 10 with a status each (spec 5.4 item 5): `pass`, `fail`, or `not run` when the row had nothing to check. */
|
|
71
77
|
sessionResults: SessionVerification[];
|
|
78
|
+
/** Per harness, the session files seen, how many yielded records, and how many were unreadable (`harness-files.ts` defines them). Read from the harness's own store on this machine; nothing is sent. */
|
|
79
|
+
harnessFiles: HarnessFiles;
|
|
72
80
|
/** Layer 4 at T0, local (spec 2.4): each session's raw-file match, recomputed root, and count bindings, with the count of each row of the result table. */
|
|
73
81
|
audit: {
|
|
74
82
|
counts: T0Counts;
|
|
@@ -119,9 +127,16 @@ export interface HookTarget {
|
|
|
119
127
|
}
|
|
120
128
|
export declare const sessionsHookInstall: (deps: SessionsDeps, options: HookTarget & {
|
|
121
129
|
keepRetention: boolean;
|
|
122
|
-
schedule: "nightly" | "off" | undefined;
|
|
130
|
+
schedule: "nightly" | "off" | "on" | undefined;
|
|
131
|
+
harness?: "codex" | undefined;
|
|
132
|
+
machine?: boolean | undefined;
|
|
133
|
+
env?: readonly string[] | undefined;
|
|
134
|
+
termsFile?: string | undefined;
|
|
135
|
+
}) => Promise<Outcome>;
|
|
136
|
+
export declare const sessionsHookUninstall: (deps: SessionsDeps, options: HookTarget & {
|
|
137
|
+
harness?: "codex" | undefined;
|
|
123
138
|
}) => Promise<Outcome>;
|
|
124
|
-
export declare const sessionsHookUninstall: (deps: SessionsDeps, options: HookTarget) => Promise<Outcome>;
|
|
125
139
|
export declare const sessionsHookStatus: (deps: SessionsDeps, options: {
|
|
126
140
|
file: string | undefined;
|
|
141
|
+
harness?: "codex" | undefined;
|
|
127
142
|
}) => Promise<Outcome>;
|
package/dist/sessions/command.js
CHANGED
|
@@ -7,7 +7,7 @@ import { CliError, ExitCode } from '../errors.js';
|
|
|
7
7
|
import { loadTerms } from './exclusion.js';
|
|
8
8
|
import { EVALUATOR_MODEL, fillArms, sidecarSaver } from './gold-arms.js';
|
|
9
9
|
import { armsPath, coverageLines, formatScore, isGoldDocument, parseGold, plural, renderGold, sampleGold, scoreGold } from './gold.js';
|
|
10
|
-
import { makeRedactor } from './redact.js';
|
|
10
|
+
import { localNames, makeRedactor } from './redact.js';
|
|
11
11
|
import { books, BOOKS_CAVEAT, formatBooks, formatMeasure, formatRecall, measure } from './query.js';
|
|
12
12
|
import { formatLineage, loadLineage, walkLineage } from './lineage.js';
|
|
13
13
|
import { recallFields } from '../telemetry/fields.js';
|
|
@@ -15,9 +15,14 @@ import { appendRecall, readRecalls } from './recall-object.js';
|
|
|
15
15
|
import { deriveConsequences, formatRecallCounts, recallCounts } from './recall-consequence.js';
|
|
16
16
|
import { pull, pullRefusal } from './recall-reads.js';
|
|
17
17
|
import { MISS_TEXT } from './recall-render.js';
|
|
18
|
+
import { receiptOf, RECEIPT_MISS } from './receipt.js';
|
|
19
|
+
import { EMPTY_RECALL, emptyBooks, nothingFoundLines, savedListNote } from './empty-state.js';
|
|
18
20
|
import { readHookCalls } from './hook-calls.js';
|
|
19
21
|
import { buildIndex, contextBlock, inRepository, readIndex } from './hook-index.js';
|
|
20
|
-
import {
|
|
22
|
+
import { CODEX_TRUST_NOTICE, codexHookStatus, installCodexHook, uninstallCodexHook } from './codex-hook-install.js';
|
|
23
|
+
import { enrollmentOf } from './machine.js';
|
|
24
|
+
import { hookStatus, installHooks, installMachineHooks, readSettings, removeSchedule, SCHEDULE_REFUSED, scheduleStatus, settingsPath, uninstallHooks } from './install.js';
|
|
25
|
+
import { recordSchedule } from './pool-due.js';
|
|
21
26
|
import { serve } from './mcp.js';
|
|
22
27
|
import { refreshPool, servedPool } from './pool.js';
|
|
23
28
|
import { recallPool } from './recall-bundle.js';
|
|
@@ -29,7 +34,8 @@ import { FAMILIES } from './read.js';
|
|
|
29
34
|
import { formatNotReadable, notReadable } from './read-readable.js';
|
|
30
35
|
import { formatReads, isFamily, parseSlice, selectReads } from './read-rows.js';
|
|
31
36
|
import { CAPTURE_VERSION, OLDEST_READABLE_CAPTURE } from './schema.js';
|
|
32
|
-
import {
|
|
37
|
+
import { harnessFileCounts } from './harness-files.js';
|
|
38
|
+
import { deposit, frozenSessions, loadStore, paths, readCalls, readRedactDomains, readSaved, readSkeletons, skippedFolders, slugOf } from './store.js';
|
|
33
39
|
import { formatStepCounters, inputKey, newContext, readSteps, stampKey, runDeposit, runStep, stepCounters, stuckSteps, tryLog } from './steps.js';
|
|
34
40
|
import { formatSelfBaseline, selfBaseline as selfBaselineOf } from './baseline.js';
|
|
35
41
|
import { staleBatchesAnywhere } from './pool-outbox.js';
|
|
@@ -70,6 +76,8 @@ export const sessionsDeposit = async (deps, options) => {
|
|
|
70
76
|
const lines = [
|
|
71
77
|
`Deposited ${String(result.sessions)} sessions and ${String(result.subagents)} subagent transcripts (${String(result.lines)} lines, ${String(result.badLines)} unparseable) as ${String(result.observations)} observations; ${String(result.unchanged)} files unchanged.`,
|
|
72
78
|
`Read ${String(result.folders.length)} folders; skipped ${String(result.skippedFolders)} not on the allowlist.`,
|
|
79
|
+
// The list an earlier run saved decides what a plain deposit reads; the run says so instead of looking like "every source".
|
|
80
|
+
...savedListNote(read, allow, paths(runtime.storeDir).state),
|
|
73
81
|
...Object.entries(result.sources).flatMap(([name, counts]) => [
|
|
74
82
|
...(counts.read === 0 ? [] : [`Read ${String(counts.read)} ${sourceLabel(name, table)} sessions.`]),
|
|
75
83
|
...counts.unreadable.map((reason) => `${sourceLabel(name, table)} store not read: ${reason}`),
|
|
@@ -79,6 +87,8 @@ export const sessionsDeposit = async (deps, options) => {
|
|
|
79
87
|
survival === null
|
|
80
88
|
? 'Commit survival not read this run.'
|
|
81
89
|
: `Commits older than ${String(SURVIVAL_DAYS)} days: ${String(survival.survived)} survived, ${String(survival.reverted)} reverted, ${String(survival.unreachable)} unreachable, ${String(survival.unknown)} unknown.`,
|
|
90
|
+
// Nothing parsed and nothing already deposited: say where it looked, so an empty result is not a silent one.
|
|
91
|
+
...(result.sessions + result.subagents + result.unchanged === 0 ? nothingFoundLines(result, runtime, table, read) : []),
|
|
82
92
|
];
|
|
83
93
|
return { document: { ok: true, ...result, survival }, text: lines.join('\n') };
|
|
84
94
|
};
|
|
@@ -97,18 +107,32 @@ export const sessionsRecall = async (deps, query, sid = null) => {
|
|
|
97
107
|
const { storeDir } = runtimeOf(deps);
|
|
98
108
|
// The key is the query and `ev`, known before the work, so a recall that keeps failing is the same key each time.
|
|
99
109
|
const key = stampKey(JSON.stringify(query));
|
|
100
|
-
const result = await runStep(newContext(storeDir, deps.now, 'manual'), 'recall', async () => {
|
|
110
|
+
const { found: result, empty } = await runStep(newContext(storeDir, deps.now, 'manual'), 'recall', async () => {
|
|
101
111
|
const store = await loadStore(storeDir);
|
|
102
112
|
const found = pull(store, query, { sid, now: deps.now(), frame: null, redactDomains: await readRedactDomains(storeDir), current: await recallPool(storeDir, store, servedPool) });
|
|
103
|
-
return { value: found, report: { in: { query: 1 }, out: { matches: found.legacy?.matches.length ?? found.reads.length }, key } };
|
|
113
|
+
return { value: { found, empty: store.sessions.length === 0 }, report: { in: { query: 1 }, out: { matches: found.legacy?.matches.length ?? found.reads.length }, key } };
|
|
104
114
|
}, key);
|
|
105
115
|
await tryLog(() => appendRecall(storeDir, result.recall));
|
|
106
116
|
// Counts and closed words only: how it was asked, the families it served, how many reads, and its arm. Never the query or the answer.
|
|
107
117
|
deps.telemetry?.('recall', recallFields(result.recall, result.reads.map((read) => read.family), result.legacy !== undefined));
|
|
118
|
+
// A miss over a store with no sessions is not "no match": the store has nothing to match, and the text names the command that fills it.
|
|
108
119
|
if (result.legacy !== undefined)
|
|
109
|
-
return { document: { ok: true, ...result.legacy, recall: result.recall }, text: formatRecall(result.legacy) };
|
|
120
|
+
return { document: { ok: true, ...result.legacy, recall: result.recall }, text: empty && result.legacy.matches.length === 0 ? EMPTY_RECALL : formatRecall(result.legacy) };
|
|
110
121
|
// The document states the recall and not the raw reads: a read's tool, keys, and versions are store strings that can carry text, and `recall.evidence` already states them closed.
|
|
111
|
-
return { document: { ok: true, recall: result.recall }, text: result.recall.answer.rendered === '' ? MISS_TEXT : result.recall.answer.rendered };
|
|
122
|
+
return { document: { ok: true, recall: result.recall }, text: result.recall.answer.rendered === '' ? (empty ? EMPTY_RECALL : MISS_TEXT) : result.recall.answer.rendered };
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* `recall --error <text> --receipt`: the same pull, recorded as any recall is, printed as a paste-ready block for an issue. Only `--error` asks about one error;
|
|
126
|
+
* every other form refuses, because a receipt states one signature.
|
|
127
|
+
*/
|
|
128
|
+
export const sessionsReceipt = async (deps, query, sid = null) => {
|
|
129
|
+
if ('form' in query || query.error === undefined || query.path !== undefined || query.cls !== undefined || query.sinceDays !== undefined) {
|
|
130
|
+
throw new CliError('--receipt answers one error: pass --error "<text>" and no other form.', ExitCode.USAGE, 'usage');
|
|
131
|
+
}
|
|
132
|
+
await sessionsRecall(deps, query, sid);
|
|
133
|
+
const { storeDir } = runtimeOf(deps);
|
|
134
|
+
const block = receiptOf(await loadStore(storeDir), query, deps.now(), await readRedactDomains(storeDir), localNames());
|
|
135
|
+
return { document: { ok: true, receipt: block }, text: block ?? RECEIPT_MISS };
|
|
112
136
|
};
|
|
113
137
|
/** `recall --counts`: push and pull, shown and held, as counts only. */
|
|
114
138
|
export const sessionsRecallCounts = async (deps, day) => {
|
|
@@ -149,12 +173,16 @@ export const sessionsContext = async (deps, options) => {
|
|
|
149
173
|
const { storeDir } = runtimeOf(deps);
|
|
150
174
|
// The manual recall verb (spec 4.1): its key is what it looks up, the repository's cwd and the harness, known before the work.
|
|
151
175
|
const key = stampKey('context', options.cwd, options.harness ?? '');
|
|
152
|
-
const
|
|
176
|
+
const step = await runStep(newContext(storeDir, deps.now, 'manual'), 'recall', async () => {
|
|
153
177
|
const index = (await readIndex(storeDir)) ?? buildIndex(await loadStore(storeDir), await readRedactDomains(storeDir), new Date(deps.now()).toISOString());
|
|
154
178
|
const found = contextBlock(index, options);
|
|
155
|
-
return { value: found, report: { in: { query: 1 }, out: { matches: found === undefined ? 0 : 1 }, key } };
|
|
179
|
+
return { value: { found, noProjects: Object.keys(index.projects).length === 0 }, report: { in: { query: 1 }, out: { matches: found === undefined ? 0 : 1 }, key } };
|
|
156
180
|
}, key);
|
|
157
|
-
|
|
181
|
+
// Nothing to inject because the store holds no sessions at all is an empty store, not a quiet repository: say so and name the command that fills it.
|
|
182
|
+
// An index that names projects answers alone; the store is loaded only when the index is empty, to tell an empty store from an index with nothing to say.
|
|
183
|
+
const block = step.found;
|
|
184
|
+
const text = block ?? (step.noProjects && (await loadStore(storeDir)).sessions.length === 0 ? EMPTY_RECALL : 'Nothing recurs in this repository yet.');
|
|
185
|
+
return { document: { ok: true, context: block ?? null }, text };
|
|
158
186
|
};
|
|
159
187
|
export const sessionsMcp = async (deps) => {
|
|
160
188
|
await serve(runtimeOf(deps), deps.now, deps.telemetry);
|
|
@@ -173,6 +201,11 @@ const skippedNotice = (scope) => {
|
|
|
173
201
|
const transcripts = scope.transcripts === 0 ? 'no main transcript' : scope.transcripts === 1 ? '1 main transcript' : `${String(scope.transcripts)} main transcripts`;
|
|
174
202
|
return [`${folders} ${transcripts}; ${one ? 'it is' : 'they are'} never read: not on the allowlist. Not captured: ${shown}${more}.`];
|
|
175
203
|
};
|
|
204
|
+
/** The three counts per harness, as numbers only: no cause, no path, no verdict. */
|
|
205
|
+
const harnessFilesLines = (files, table) => {
|
|
206
|
+
const rows = Object.entries(files).map(([harness, counts]) => ` ${harness === 'claude-code' ? 'Claude Code' : sourceLabel(harness, table)}: seen ${String(counts.seen)}, yielded ${String(counts.yielded)}, unreadable ${String(counts.unreadable)}`);
|
|
207
|
+
return rows.length === 0 ? [] : ['Session files per harness:', ...rows];
|
|
208
|
+
};
|
|
176
209
|
/** Scope is an extra, like the step log: a projects root this process cannot list reports an empty scope and fails neither `verify` nor `books`. */
|
|
177
210
|
const scopeOf = (runtime) => skippedFolders(runtime).catch(() => ({ folders: 0, transcripts: 0, names: [] }));
|
|
178
211
|
/** One line for a store a newer oars wrote into (spec state-on-open row for a newer `cv`: read known fields, warn). */
|
|
@@ -205,7 +238,9 @@ const packageBooksOutcome = async (deps, options) => {
|
|
|
205
238
|
const [{ excluded }, store] = await Promise.all([scanStore(runtime, options.termsFile), loadStore(runtime.storeDir)]);
|
|
206
239
|
const cutoff = options.sinceDays === undefined ? undefined : new Date(deps.now() - options.sinceDays * 86_400_000).toISOString();
|
|
207
240
|
const result = packageBooks(store, new Set(excluded.map((entry) => entry.session)), vocabulary(), cutoff);
|
|
208
|
-
|
|
241
|
+
// The same empty-store text the other `books` forms print, so it names the command that fills the store.
|
|
242
|
+
const table = store.sessions.length === 0 && store.prompts.length === 0 ? emptyBooks('public package pulls') : formatPackageBooks(result);
|
|
243
|
+
return { document: { ok: true, by: 'package', ...result, newerCaptures: store.newerCaptures }, text: [table, ...newerNotice(store)].join('\n') };
|
|
209
244
|
};
|
|
210
245
|
export const sessionsBooks = async (deps, options) => {
|
|
211
246
|
if (options.by === 'package') {
|
|
@@ -220,11 +255,13 @@ export const sessionsBooks = async (deps, options) => {
|
|
|
220
255
|
const rows = books(store, options.by, deps.now(), options.sinceDays, await readSurvival(storeDir));
|
|
221
256
|
// A mistyped root, or a --since window nothing falls in, over a populated store must not read as an empty store.
|
|
222
257
|
const emptyWindow = rows.length === 0 && options.sinceDays !== undefined && store.prompts.length > 0;
|
|
223
|
-
const table =
|
|
224
|
-
?
|
|
225
|
-
:
|
|
226
|
-
? `No ${options.
|
|
227
|
-
:
|
|
258
|
+
const table = loaded.sessions.length === 0 && loaded.prompts.length === 0
|
|
259
|
+
? emptyBooks(options.by === 'pull' ? 'pulls' : 'prompts')
|
|
260
|
+
: store.prompts.length === 0 && options.repo !== undefined
|
|
261
|
+
? `No prompts for ${options.repo} in the store (${String(loaded.prompts.length)} prompts across all repositories).`
|
|
262
|
+
: emptyWindow
|
|
263
|
+
? `No ${options.by === 'pull' ? 'pulls' : 'prompts'} in the --since window.`
|
|
264
|
+
: formatBooks(rows, options.by);
|
|
228
265
|
const scope = await scopeOf(runtime);
|
|
229
266
|
const bundle = await lastFetchedBundle(storeDir);
|
|
230
267
|
return { document: { ok: true, by: options.by, rows, newerCaptures: store.newerCaptures, scope, pool: bundle?.books ?? null }, text: [table, ...skippedNotice(scope), ...newerNotice(store), '', ...booksLines(bundle), ...(rows.length === 0 ? [] : ['', BOOKS_CAVEAT])].join('\n') };
|
|
@@ -271,10 +308,12 @@ export const sessionsVerify = async (deps, options) => {
|
|
|
271
308
|
// A verify that got here has succeeded, so a verify streak is the one this run ends and is not listed.
|
|
272
309
|
const stuck = stuckSteps(await readSteps(storeDir).catch(() => [])).filter((entry) => entry.step !== 'verify');
|
|
273
310
|
const scope = await scopeOf(runtime);
|
|
311
|
+
// Like the scope, an extra: a store that cannot be read is an unreadable count inside it, and a failure outside it leaves the section out.
|
|
312
|
+
const harnessFiles = await harnessFileCounts(runtime).catch(() => ({}));
|
|
274
313
|
// A local count like the scope: an outbox folder that cannot be read is no reason for `verify` to fail.
|
|
275
314
|
const staleBatches = await staleBatchesAnywhere(storeDir).catch(() => 0);
|
|
276
315
|
const totals = await outboxTotals(storeDir).catch(() => ({ sentBatches: 0, sentRaw: 0, rawCopies: 0, rawMasked: 0 }));
|
|
277
|
-
const report = { sessions: skeletons.length, unreadable, cvFrozen: frozen.size, scope, invariants: results.map((result) => ({ ...result, pass: result.violations === 0 })), stuck, sessionResults, audit, selfBaseline, staleBatches, ...totals };
|
|
316
|
+
const report = { sessions: skeletons.length, unreadable, cvFrozen: frozen.size, scope, harnessFiles, invariants: results.map((result) => ({ ...result, pass: result.violations === 0 })), stuck, sessionResults, audit, selfBaseline, staleBatches, ...totals };
|
|
278
317
|
const failed = [...results.filter((result) => result.violations > 0).map((result) => result.name), ...(unreadable > 0 ? ['unreadable skeletons'] : []), ...merkleFailed, ...stuck.map((entry) => `stuck: ${entry.step}`)];
|
|
279
318
|
const stuckLines = stuck.map((entry) => `FAIL ${entry.step} is stuck: key ${entry.key} failed ${String(entry.runs)} runs in a row: ${entry.error}`);
|
|
280
319
|
const highWater = mine([...store.sessions, ...store.subagents]).map((session) => `${session.session}:${String(session.rev)}:${session.cv}`).sort();
|
|
@@ -283,7 +322,7 @@ export const sessionsVerify = async (deps, options) => {
|
|
|
283
322
|
failed,
|
|
284
323
|
report,
|
|
285
324
|
document: { ok: true, ...report, newerCaptures: store.newerCaptures },
|
|
286
|
-
text: [formatVerify(results, skeletons.length), formatAudit(audits), formatSelfBaseline(selfBaseline), ...(resultLines === '' ? [] : [resultLines]), ...(frozen.size > 0 ? [`${String(frozen.size)} cv-frozen sessions: behind the current capture version, but the transcript is gone from the harness and raw/ (or, for a Cursor session, the composer is gone from Cursor), so they cannot be captured again`] : []), ...skippedNotice(scope), ...(staleBatches > 0 ? [`${String(staleBatches)} outbox batches were built under an older vocabulary, field list, or skeleton format; they will never be sent, and oars pool deposit removes them`] : []), ...(totals.sentBatches + totals.sentRaw > 0 ? [`${String(totals.sentBatches)} outbox ${totals.sentBatches === 1 ? 'batch' : 'batches'} and ${String(totals.sentRaw)} raw ${totals.sentRaw === 1 ? 'copy were' : 'copies were'} already sent`] : []), ...(totals.rawCopies > 0 ? [`${String(totals.rawCopies)} queued raw ${totals.rawCopies === 1 ? 'copy holds' : 'copies hold'} ${String(totals.rawMasked)} placeholders (masked values, plus any a transcript already held as text)`] : []), ...newerNotice(store), ...(unreadable > 0 ? [`FAIL ${String(unreadable)} unreadable skeleton files (torn or corrupted); redeposit to rebuild them`] : []), ...stuckLines].join('\n'),
|
|
325
|
+
text: [formatVerify(results, skeletons.length), formatAudit(audits), formatSelfBaseline(selfBaseline), ...(resultLines === '' ? [] : [resultLines]), ...(frozen.size > 0 ? [`${String(frozen.size)} cv-frozen sessions: behind the current capture version, but the transcript is gone from the harness and raw/ (or, for a Cursor session, the composer is gone from Cursor), so they cannot be captured again`] : []), ...harnessFilesLines(harnessFiles, runtime.sources ?? SOURCES), ...skippedNotice(scope), ...(staleBatches > 0 ? [`${String(staleBatches)} outbox batches were built under an older vocabulary, field list, or skeleton format; they will never be sent, and oars pool deposit removes them`] : []), ...(totals.sentBatches + totals.sentRaw > 0 ? [`${String(totals.sentBatches)} outbox ${totals.sentBatches === 1 ? 'batch' : 'batches'} and ${String(totals.sentRaw)} raw ${totals.sentRaw === 1 ? 'copy were' : 'copies were'} already sent`] : []), ...(totals.rawCopies > 0 ? [`${String(totals.rawCopies)} queued raw ${totals.rawCopies === 1 ? 'copy holds' : 'copies hold'} ${String(totals.rawMasked)} placeholders (masked values, plus any a transcript already held as text)`] : []), ...newerNotice(store), ...(unreadable > 0 ? [`FAIL ${String(unreadable)} unreadable skeleton files (torn or corrupted); redeposit to rebuild them`] : []), ...stuckLines].join('\n'),
|
|
287
326
|
};
|
|
288
327
|
const cvBehind = results.find((result) => result.name === CAPTURE_INVARIANTS[0])?.violations ?? 0;
|
|
289
328
|
// The key is the sessions checked, each at its rev and cv, and the code's ev (spec 4.2).
|
|
@@ -373,47 +412,120 @@ const installOf = (deps) => {
|
|
|
373
412
|
throw new CliError('Hook install is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
374
413
|
return deps.install;
|
|
375
414
|
};
|
|
415
|
+
const codexOf = (deps) => {
|
|
416
|
+
const codex = installOf(deps).codex;
|
|
417
|
+
if (codex === undefined)
|
|
418
|
+
throw new CliError('The Codex hook is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
419
|
+
return codex;
|
|
420
|
+
};
|
|
421
|
+
/** `--harness codex` writes to the Codex home, so a settings-file flag beside it names a target that does not apply. */
|
|
422
|
+
const refuseSettingsFlags = (options) => {
|
|
423
|
+
if (options.scope !== undefined || options.file !== undefined)
|
|
424
|
+
throw new CliError('--harness codex writes hooks.json in the Codex home; --scope and --file apply to Claude Code settings only.', ExitCode.USAGE, 'usage');
|
|
425
|
+
};
|
|
376
426
|
/** `--scope`, else `file` when `--file` names one, else the user settings. */
|
|
377
427
|
const hookScope = (options) => options.scope ?? (options.file === undefined ? 'user' : 'file');
|
|
428
|
+
/** What `--schedule off` and `--schedule on` say: the recorded choice about the background sweep and send that the session-end hook and every command start. */
|
|
429
|
+
const SENDS_OFF = 'The background sweep and the pool send are off, as you chose with --schedule off (telemetry has its own switch: oars telemetry off). Turn them back on with: oars sessions hook install --schedule on';
|
|
430
|
+
const SENDS_ON = 'Background sweeps and sends are on: a due sweep and send starts from the session-end hook and from any oars command, at most about hourly.';
|
|
431
|
+
/** `--machine`: the Stop and SessionEnd entries for a cloud VM, a VPS, or a runner, and for a user-scope file the enrollment block too. Refused beside every flag it does not take, before anything is written. */
|
|
432
|
+
const installMachine = async (deps, options) => {
|
|
433
|
+
const refuse = (message) => new CliError(message, ExitCode.USAGE, 'usage');
|
|
434
|
+
if (options.harness !== undefined || options.schedule !== undefined)
|
|
435
|
+
throw refuse('--machine writes the Stop and SessionEnd entries of a machine; --harness and --schedule do not apply to it.');
|
|
436
|
+
if (options.keepRetention === true)
|
|
437
|
+
throw refuse('--keep-retention does not apply to --machine: a machine install leaves cleanupPeriodDays alone.');
|
|
438
|
+
const scope = hookScope(options);
|
|
439
|
+
if (scope === 'file')
|
|
440
|
+
throw refuse('--machine takes --scope project (the .claude/settings.json of a repository, committed) or --scope user (the ~/.claude/settings.json of a VPS or runner).');
|
|
441
|
+
if (options.file !== undefined)
|
|
442
|
+
throw refuse('--file does not apply to --machine: it writes the user or project settings file named by --scope.');
|
|
443
|
+
const runtime = installOf(deps);
|
|
444
|
+
const file = settingsPath(runtime, scope, undefined);
|
|
445
|
+
const lines = [];
|
|
446
|
+
let result;
|
|
447
|
+
if (scope === 'project') {
|
|
448
|
+
if ((options.env ?? []).length > 0 || options.termsFile !== undefined)
|
|
449
|
+
throw refuse('--env and --terms-file belong to --scope user: a project settings file is committed, and the enrollment block holds salts and terms.');
|
|
450
|
+
result = await installMachineHooks(file, undefined);
|
|
451
|
+
lines.push(`Installed ${result.installed.join(' and ')} machine hooks in ${file}. They run only where OARS_HOST_CLASS is set to a class other than local; set it, and the rest of the enrollment block, in the environment that runs the session.`);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
// A file that cannot be read is refused before the terms file is written.
|
|
455
|
+
await readSettings(file);
|
|
456
|
+
const enrollment = await enrollmentOf(options.env ?? [], options.termsFile, runtime.cwd);
|
|
457
|
+
result = await installMachineHooks(file, enrollment.env);
|
|
458
|
+
lines.push(`Installed ${result.installed.join(' and ')} machine hooks and the enrollment block in ${file}, which is now readable by its owner only.`);
|
|
459
|
+
if (enrollment.termsPath !== undefined)
|
|
460
|
+
lines.push(`The exclusion terms are in ${enrollment.termsPath} (owner-only), named by OARS_EXCLUSION_TERMS.`);
|
|
461
|
+
if (enrollment.env.OARS_EXCLUSION_TERMS === undefined && enrollment.env.OARS_EXCLUSION_TERMS_INLINE === undefined)
|
|
462
|
+
lines.push('No exclusion terms were given: with no term file every session is excluded (fail-closed), so nothing will be built.');
|
|
463
|
+
}
|
|
464
|
+
return { document: { ok: true, machine: true, install: result }, text: lines.join('\n') };
|
|
465
|
+
};
|
|
378
466
|
export const sessionsHookInstall = async (deps, options) => {
|
|
467
|
+
if (options.machine === true)
|
|
468
|
+
return installMachine(deps, options);
|
|
469
|
+
if ((options.env ?? []).length > 0 || options.termsFile !== undefined)
|
|
470
|
+
throw new CliError('--env and --terms-file belong to --machine.', ExitCode.USAGE, 'usage');
|
|
471
|
+
if (options.harness === 'codex') {
|
|
472
|
+
refuseSettingsFlags(options);
|
|
473
|
+
if (options.schedule !== undefined)
|
|
474
|
+
throw new CliError('--schedule applies to the Claude Code hooks, not --harness codex.', ExitCode.USAGE, 'usage');
|
|
475
|
+
const result = await installCodexHook(codexOf(deps));
|
|
476
|
+
return { document: { ok: true, harness: 'codex', ...result, trust: 'asked by codex' }, text: `Installed ${result.installed.join(', ')} hooks in ${result.file}.\n${CODEX_TRUST_NOTICE}` };
|
|
477
|
+
}
|
|
379
478
|
const runtime = installOf(deps);
|
|
479
|
+
// `--schedule nightly` asked for the task this version no longer registers: refused before anything is written.
|
|
480
|
+
if (options.schedule === 'nightly')
|
|
481
|
+
throw new CliError(SCHEDULE_REFUSED, ExitCode.USAGE, 'usage');
|
|
380
482
|
const lines = [];
|
|
381
483
|
const document = { ok: true };
|
|
382
|
-
// `--schedule` alone touches no settings file
|
|
484
|
+
// `--schedule off` or `on` alone touches no settings file; it removes the old task and records the choice.
|
|
383
485
|
if (options.schedule === undefined || options.scope !== undefined || options.file !== undefined) {
|
|
384
486
|
const file = settingsPath(runtime, hookScope(options), options.file);
|
|
385
487
|
const result = await installHooks(runtime, file, options.keepRetention);
|
|
386
488
|
document.install = result;
|
|
387
489
|
lines.push(`Installed ${result.installed.join(', ')} hooks in ${file}.`, result.retention);
|
|
388
490
|
}
|
|
389
|
-
|
|
390
|
-
if (
|
|
491
|
+
const removed = await removeSchedule(runtime);
|
|
492
|
+
if (removed !== undefined) {
|
|
493
|
+
document.schedule = removed;
|
|
494
|
+
lines.push(removed);
|
|
495
|
+
}
|
|
496
|
+
if (options.schedule !== undefined) {
|
|
391
497
|
const storeDir = deps.sessions?.storeDir;
|
|
392
|
-
|
|
393
|
-
let line = SCHEDULE_LEFT_OFF;
|
|
394
|
-
if (options.schedule !== undefined) {
|
|
395
|
-
line = await setSchedule(runtime, options.schedule);
|
|
498
|
+
if (storeDir !== undefined)
|
|
396
499
|
await recordSchedule(storeDir, options.schedule);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
lines.push(line);
|
|
500
|
+
document.sends = options.schedule;
|
|
501
|
+
// An explicit `off` that found no task still says why nothing was removed, so the output is never empty.
|
|
502
|
+
if (options.schedule === 'off' && removed === undefined)
|
|
503
|
+
lines.push(`No scheduled task to remove${runtime.platform === 'win32' ? '' : ' (only Windows ever had one)'}.`);
|
|
504
|
+
lines.push(options.schedule === 'off' ? SENDS_OFF : SENDS_ON);
|
|
403
505
|
}
|
|
404
506
|
return { document, text: lines.join('\n') };
|
|
405
507
|
};
|
|
406
508
|
export const sessionsHookUninstall = async (deps, options) => {
|
|
509
|
+
if (options.harness === 'codex') {
|
|
510
|
+
refuseSettingsFlags(options);
|
|
511
|
+
const result = await uninstallCodexHook(codexOf(deps));
|
|
512
|
+
return { document: { ok: true, harness: 'codex', ...result }, text: result.removed.length === 0 ? `No Many Oars hooks in ${result.file}.` : `Removed ${result.removed.join(', ')} hooks from ${result.file}; other hooks are untouched.` };
|
|
513
|
+
}
|
|
407
514
|
const runtime = installOf(deps);
|
|
408
515
|
const file = settingsPath(runtime, hookScope(options), options.file);
|
|
409
516
|
const result = await uninstallHooks(file);
|
|
517
|
+
const removed = await removeSchedule(runtime);
|
|
410
518
|
const text = result.removed.length === 0 ? `No Many Oars hooks in ${file}.` : `Removed ${result.removed.join(', ')} hooks from ${file}; other settings and hooks are untouched (cleanupPeriodDays is left as it is).`;
|
|
411
|
-
return { document: { ok: true, ...result }, text };
|
|
519
|
+
return { document: { ok: true, ...result, ...(removed === undefined ? {} : { schedule: removed }) }, text: removed === undefined ? text : `${text}\n${removed}` };
|
|
412
520
|
};
|
|
413
521
|
export const sessionsHookStatus = async (deps, options) => {
|
|
522
|
+
if (options.harness === 'codex') {
|
|
523
|
+
const result = await codexHookStatus(codexOf(deps));
|
|
524
|
+
return { document: { ok: true, harness: 'codex', ...result }, text: `codex: ${result.file}: ${result.installed.length === 0 ? 'no hooks' : result.installed.join(', ')}` };
|
|
525
|
+
}
|
|
414
526
|
const runtime = installOf(deps);
|
|
415
527
|
const files = await hookStatus(runtime, options.file);
|
|
416
528
|
const schedule = await scheduleStatus(runtime);
|
|
417
529
|
const lines = files.map((item) => `${item.scope}: ${item.file}: ${item.installed.length === 0 ? 'no hooks' : item.installed.join(', ')} (cleanupPeriodDays ${String(item.retentionDays)})`);
|
|
418
|
-
return { document: { ok: true, files, schedule }, text: [...lines, schedule].join('\n') };
|
|
530
|
+
return { document: { ok: true, files, ...(schedule === undefined ? {} : { schedule }) }, text: (schedule === undefined ? lines : [...lines, schedule]).join('\n') };
|
|
419
531
|
};
|
|
@@ -24,6 +24,8 @@ export interface CopilotStore {
|
|
|
24
24
|
sessions: CopilotSession[];
|
|
25
25
|
/** The session as transcript text for `captureSession`; throws when the file cannot be read or holds no parseable line. */
|
|
26
26
|
transcript: (session: CopilotSession) => Promise<string>;
|
|
27
|
+
/** True when the log holds only head records (`eventHeadOnly`); `sessions verify` counts such a file as empty, not unreadable. */
|
|
28
|
+
headOnly: (session: CopilotSession) => Promise<boolean>;
|
|
27
29
|
/** Folders skipped because their name is not a safe file name (it names a skeleton), counted so the skip is never silent. */
|
|
28
30
|
rejected: number;
|
|
29
31
|
/** Sessions that could not be inspected, reported once each and never fatal; the reason names no path. */
|
|
@@ -34,6 +36,7 @@ export interface CopilotStore {
|
|
|
34
36
|
}
|
|
35
37
|
/** The non-blank lines that are not JSON (a torn write), kept so `captureSession` counts them and the deposit line reports them. */
|
|
36
38
|
export declare const eventBadLines: (text: string) => string[];
|
|
39
|
+
export declare const eventHeadOnly: (text: string) => boolean;
|
|
37
40
|
/** One session as the lines `captureSession` reads. */
|
|
38
41
|
export declare const eventLines: (text: string, fallbackId: string) => Obj[];
|
|
39
42
|
export declare const openCopilotStore: (root: string) => Promise<CopilotStore | undefined>;
|