@manyoars/oars 0.3.0 → 0.4.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 +44 -29
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +102 -32
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/gate/hook-main.js +13 -1
- package/dist/gate/hook-process.d.ts +11 -1
- package/dist/gate/hook-process.js +50 -2
- package/dist/gate/hook-record.d.ts +3 -0
- package/dist/gate/hook-record.js +8 -0
- package/dist/gate/hook.d.ts +2 -0
- package/dist/gate/hook.js +3 -0
- package/dist/gate/replay.js +2 -5
- package/dist/gate/report.js +1 -1
- package/dist/gate/transcripts.js +1 -1
- package/dist/hook-bin.js +27 -2
- package/dist/issuer.d.ts +7 -9
- package/dist/issuer.js +10 -32
- package/dist/main.js +3 -0
- package/dist/sessions/actions.js +78 -0
- package/dist/sessions/allow.d.ts +10 -1
- package/dist/sessions/allow.js +29 -9
- package/dist/sessions/baseline.d.ts +1 -1
- package/dist/sessions/baseline.js +6 -4
- package/dist/sessions/call-shape.d.ts +19 -0
- package/dist/sessions/call-shape.js +85 -0
- package/dist/sessions/cell-increments.d.ts +5 -1
- package/dist/sessions/cell-increments.js +84 -50
- package/dist/sessions/cells.d.ts +33 -3
- package/dist/sessions/cells.js +89 -9
- package/dist/sessions/codex.js +93 -27
- package/dist/sessions/command.d.ts +11 -1
- package/dist/sessions/command.js +64 -18
- package/dist/sessions/coverage.d.ts +19 -0
- package/dist/sessions/coverage.js +56 -0
- package/dist/sessions/cursor.js +2 -2
- package/dist/sessions/derive.js +72 -6
- package/dist/sessions/exclusion.d.ts +6 -0
- package/dist/sessions/exclusion.js +8 -0
- package/dist/sessions/extract.d.ts +4 -2
- package/dist/sessions/extract.js +58 -26
- package/dist/sessions/hook-calls.js +1 -1
- package/dist/sessions/hook-index.d.ts +14 -1
- package/dist/sessions/hook-index.js +10 -3
- package/dist/sessions/hooks.d.ts +5 -1
- package/dist/sessions/hooks.js +133 -22
- package/dist/sessions/identity.d.ts +8 -3
- package/dist/sessions/identity.js +45 -15
- package/dist/sessions/install.d.ts +18 -2
- package/dist/sessions/install.js +53 -8
- package/dist/sessions/mcp.d.ts +3 -2
- package/dist/sessions/mcp.js +8 -4
- package/dist/sessions/motif.d.ts +2 -1
- package/dist/sessions/motif.js +2 -2
- package/dist/sessions/opencode.d.ts +33 -0
- package/dist/sessions/opencode.js +319 -0
- package/dist/sessions/otel.d.ts +60 -0
- package/dist/sessions/otel.js +494 -0
- package/dist/sessions/package-books.d.ts +48 -0
- package/dist/sessions/package-books.js +120 -0
- package/dist/sessions/package-resolve.d.ts +2 -0
- package/dist/sessions/package-resolve.js +3 -1
- package/dist/sessions/pi.d.ts +36 -0
- package/dist/sessions/pi.js +353 -0
- package/dist/sessions/pool-books.d.ts +21 -0
- package/dist/sessions/pool-books.js +75 -0
- package/dist/sessions/pool-choices.d.ts +5 -0
- package/dist/sessions/pool-choices.js +11 -0
- package/dist/sessions/pool-command.d.ts +76 -5
- package/dist/sessions/pool-command.js +263 -136
- package/dist/sessions/pool-nightly-deps.d.ts +3 -0
- package/dist/sessions/pool-nightly-deps.js +11 -0
- package/dist/sessions/pool-nightly.d.ts +10 -0
- package/dist/sessions/pool-nightly.js +85 -0
- package/dist/sessions/pool-outbox.d.ts +15 -2
- package/dist/sessions/pool-outbox.js +79 -26
- package/dist/sessions/pool-print.d.ts +1 -1
- package/dist/sessions/pool-raw.d.ts +27 -0
- package/dist/sessions/pool-raw.js +68 -0
- package/dist/sessions/pool-replica.d.ts +35 -0
- package/dist/sessions/pool-replica.js +91 -0
- package/dist/sessions/pool-send-lock.d.ts +20 -0
- package/dist/sessions/pool-send-lock.js +87 -0
- package/dist/sessions/pool-sent.d.ts +4 -0
- package/dist/sessions/pool-sent.js +26 -0
- package/dist/sessions/pool-totals.d.ts +10 -0
- package/dist/sessions/pool-totals.js +21 -0
- package/dist/sessions/pool-wire.d.ts +240 -30
- package/dist/sessions/pool-wire.js +695 -215
- package/dist/sessions/public-vocabulary.d.ts +2 -3
- package/dist/sessions/public-vocabulary.js +2 -9
- package/dist/sessions/query.d.ts +16 -4
- package/dist/sessions/query.js +32 -10
- package/dist/sessions/read-cost.js +14 -3
- package/dist/sessions/read-recovery-edge.d.ts +2 -0
- package/dist/sessions/read-recovery-edge.js +26 -0
- package/dist/sessions/read.d.ts +8 -1
- package/dist/sessions/read.js +6 -3
- package/dist/sessions/recall-bundle.d.ts +12 -0
- package/dist/sessions/recall-bundle.js +33 -0
- package/dist/sessions/recall-frames.d.ts +18 -0
- package/dist/sessions/recall-frames.js +44 -0
- package/dist/sessions/recall-next.d.ts +145 -0
- package/dist/sessions/recall-next.js +241 -0
- package/dist/sessions/recall-object.d.ts +2 -0
- package/dist/sessions/recall-object.js +1 -0
- package/dist/sessions/recall-push.d.ts +2 -2
- package/dist/sessions/recall-push.js +3 -51
- package/dist/sessions/recall-reads.d.ts +7 -0
- package/dist/sessions/recall-reads.js +7 -0
- package/dist/sessions/recovery-calls.d.ts +21 -0
- package/dist/sessions/recovery-calls.js +25 -0
- package/dist/sessions/redact.d.ts +11 -0
- package/dist/sessions/redact.js +59 -0
- package/dist/sessions/schema.d.ts +59 -9
- package/dist/sessions/schema.js +22 -3
- package/dist/sessions/session-shape.d.ts +6 -3
- package/dist/sessions/session-shape.js +19 -6
- package/dist/sessions/sources.js +75 -1
- package/dist/sessions/store.d.ts +13 -1
- package/dist/sessions/store.js +36 -16
- package/dist/sessions/survival.d.ts +2 -0
- package/dist/sessions/survival.js +2 -2
- package/dist/sessions/transcript-tail.d.ts +14 -0
- package/dist/sessions/transcript-tail.js +64 -0
- package/dist/sessions/vscode-copilot.js +24 -7
- package/dist/telemetry/command.d.ts +8 -0
- package/dist/telemetry/command.js +85 -0
- package/dist/telemetry/consent.d.ts +17 -0
- package/dist/telemetry/consent.js +63 -0
- package/dist/telemetry/crash.d.ts +8 -0
- package/dist/telemetry/crash.js +52 -0
- package/dist/telemetry/emit.d.ts +31 -0
- package/dist/telemetry/emit.js +42 -0
- package/dist/telemetry/fields.d.ts +16 -0
- package/dist/telemetry/fields.js +26 -0
- package/dist/telemetry/health.d.ts +21 -0
- package/dist/telemetry/health.js +108 -0
- package/dist/telemetry/hook-emit.d.ts +4 -0
- package/dist/telemetry/hook-emit.js +18 -0
- package/dist/telemetry/measures.d.ts +32 -0
- package/dist/telemetry/measures.js +128 -0
- package/dist/telemetry/observe.d.ts +13 -0
- package/dist/telemetry/observe.js +51 -0
- package/dist/telemetry/read-events.d.ts +9 -0
- package/dist/telemetry/read-events.js +17 -0
- package/dist/telemetry/record.d.ts +17 -0
- package/dist/telemetry/record.js +26 -0
- package/dist/telemetry/report.d.ts +77 -0
- package/dist/telemetry/report.js +188 -0
- package/dist/telemetry/schema.d.ts +40 -0
- package/dist/telemetry/schema.js +187 -0
- package/dist/telemetry/send-main.d.ts +1 -0
- package/dist/telemetry/send-main.js +11 -0
- package/dist/telemetry/send.d.ts +36 -0
- package/dist/telemetry/send.js +172 -0
- package/dist/telemetry/spool.d.ts +17 -0
- package/dist/telemetry/spool.js +70 -0
- package/dist/telemetry/wire.d.ts +25 -0
- package/dist/telemetry/wire.js +76 -0
- package/package.json +2 -2
package/dist/sessions/codex.js
CHANGED
|
@@ -36,12 +36,16 @@
|
|
|
36
36
|
// `declined` one (exit code -1, events.rs:552-554) settles as a refusal only where its output is the user's refusal text (`exec command rejected by user`), and nothing otherwise. A `failed` one with exit code -1 is a failure with no exit status (a tool
|
|
37
37
|
// failure message, events.rs:538-542; a process with none, async_watcher.rs:213, :409), so its text is the error line. So a backgrounded process keeps its failure and its commit sha
|
|
38
38
|
// even with no `write_stdin` poll; only a Legacy rollout leaves it unsettled until a poll, and there the poll settles it
|
|
39
|
+
// response_item custom_tool_call `exec` / function_call `wait` Codex 0.166+ code mode (the live 0.166/0.167 store, 2026-11-06; upstream rust-v0.167.0 not yet read): `exec`'s input is a JavaScript cell, not a command; its
|
|
40
|
+
// output opens `Script completed`, `Script failed` (+ a `Script error:` part), or `Script running with cell ID <id>`, which is the cell's outcome and never its
|
|
41
|
+
// commands'. `wait {cell_id}` polls a cell with the same three headers and settles it, in Paginated mode too
|
|
42
|
+
// event_msg item_completed CommandExecution, id `exec-<uuid>` (no call owns it, 0.166+) the command a cell ran, as its own Bash call: exit_code, aggregated_output, duration
|
|
39
43
|
// event_msg mcp_tool_call_end {call_id, result} an MCP call's outcome: `result.Err`, or `result.Ok.isError` (protocol.rs:3047, :3076); Legacy mode only
|
|
40
44
|
// event_msg patch_apply_end {call_id, success, stderr, status} an apply_patch call's outcome (Legacy mode only); a declined one settles as a refusal only where its stderr is the user's refusal text (`patch rejected by user`), and nothing otherwise, and a
|
|
41
45
|
// patch with no end event stays unsettled (a rejected patch is only a `RespondToModel` output, tools/handlers/apply_patch.rs)
|
|
42
46
|
// event_msg exec_command_end never persisted at ac558e2 (policy.rs:165) nor at rust-v0.46.0, so it is not read: the CommandExecution item and the call's output are the two sources
|
|
43
47
|
// Older shapes: the JSON `{output, metadata.exit_code}` wrapper of a shell output (rust-v0.46.0 core/src/tools/mod.rs:186-210).
|
|
44
|
-
// What Codex has no equivalent of (cost) stays at the value capture gives an absent field; message ids and
|
|
48
|
+
// What Codex has no equivalent of (cost) stays at the value capture gives an absent field; message ids and a call reply's wall time are persisted and not read (a code-mode command item's `duration` is).
|
|
45
49
|
import { createReadStream } from 'node:fs';
|
|
46
50
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
47
51
|
import { homedir } from 'node:os';
|
|
@@ -49,7 +53,7 @@ import { basename, join } from 'node:path';
|
|
|
49
53
|
import { createInterface } from 'node:readline';
|
|
50
54
|
import { PassThrough, pipeline } from 'node:stream';
|
|
51
55
|
import { createZstdDecompress, zstdDecompressSync } from 'node:zlib';
|
|
52
|
-
import {
|
|
56
|
+
import { INTERRUPT_MARKER, SHAPE_TOOLS_LINE } from './session-shape.js';
|
|
53
57
|
/** The Codex home for an environment; a test passes its own so it never reads the real one. */
|
|
54
58
|
export const codexRoot = (env, home = homedir()) => {
|
|
55
59
|
const set = env.CODEX_HOME;
|
|
@@ -140,14 +144,16 @@ const iso = (value) => {
|
|
|
140
144
|
return text !== undefined && Date.parse(text) >= Date.parse('2000-01-01T00:00:00Z') ? text : undefined;
|
|
141
145
|
};
|
|
142
146
|
/** The tool vocabulary prompt classes and `files` are computed in; a Codex name with no counterpart keeps its own. */
|
|
143
|
-
const SHELL_CALLS = new Set(['shell', 'shell_command', 'exec_command', 'local_shell', 'container.exec', 'write_stdin']);
|
|
147
|
+
const SHELL_CALLS = new Set(['shell', 'shell_command', 'exec_command', 'local_shell', 'container.exec', 'write_stdin', 'exec', 'wait']);
|
|
144
148
|
const PATCH_CALLS = new Set(['apply_patch']);
|
|
145
149
|
/** The namespace upstream fills in for a top-level function (codex-rs/protocol/src/tool_name.rs:6-7); with none and an empty one, it is the plain tool (:46-50). */
|
|
146
150
|
const DEFAULT_FUNCTION_NAMESPACE = 'functions';
|
|
151
|
+
/** One word of an argv as a POSIX shell reads it back: left alone when it holds only safe characters, else in single quotes, so `bash -lc 'git revert HEAD'` keeps its script as one word. */
|
|
152
|
+
const quotedWord = (word) => (/^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`);
|
|
147
153
|
const commandOf = (args) => {
|
|
148
154
|
const command = args.command ?? args.cmd;
|
|
149
155
|
if (Array.isArray(command))
|
|
150
|
-
return command.filter((part) => typeof part === 'string').join(' ');
|
|
156
|
+
return command.filter((part) => typeof part === 'string').map(quotedWord).join(' ');
|
|
151
157
|
return str(command);
|
|
152
158
|
};
|
|
153
159
|
/** The paths an `apply_patch` input touches (`*** Add|Update|Delete File: <path>`, the grammar of codex-rs/apply-patch). */
|
|
@@ -155,13 +161,21 @@ const patchFiles = (input) => [...input.matchAll(/^\*\*\* (?:Add|Update|Delete)
|
|
|
155
161
|
const EXIT_LINE = /^(?:Exit code: |Process exited with code )(-?\d+)\s*$/m;
|
|
156
162
|
const OUTPUT_MARK = /(?:^|\n)Output:\n?/;
|
|
157
163
|
const RUNNING_LINE = /^Process running with session ID (\S+)\s*$/m;
|
|
164
|
+
/**
|
|
165
|
+
* A code-mode cell's outcome is its output's first line (the live 0.166/0.167 store, 2026-11-06; upstream rust-v0.167.0 not yet read): `Script completed`, `Script failed` (a second part
|
|
166
|
+
* `Script error:\n<message>` follows), or `Script running with cell ID <id>`. It is the cell's outcome, never its commands': a completed cell can hold
|
|
167
|
+
* commands that exited nonzero, and those are CommandExecution items of their own.
|
|
168
|
+
*/
|
|
169
|
+
const SCRIPT_HEAD = /^Script (completed|failed|running with cell ID (\S+))[ \t]*(?:\n|$)/;
|
|
170
|
+
const SCRIPT_ERROR = /(?:^|\n)Script error:\n?/;
|
|
158
171
|
/**
|
|
159
172
|
* The message of a call that failed before it produced an exit code, written as the whole output (no `Output:` header): `execution error: ` for a
|
|
160
173
|
* shell call (core/src/tools/events.rs:438), and for the default unified-exec tool (ConfigShellToolType is only `UnifiedExec | Disabled`,
|
|
161
174
|
* protocol/src/openai_models.rs:330-333) `exec_command failed: ` (core/src/tools/handlers/unified_exec/exec_command.rs:270), `write_stdin failed: `
|
|
162
|
-
* (write_stdin.rs:123), and `write_stdin approval failed: ` (:121). `
|
|
175
|
+
* (write_stdin.rs:123), and `write_stdin approval failed: ` (:121). `wait failed: ` is assumed by analogy with `write_stdin failed: ` (write_stdin.rs:123) and has not
|
|
176
|
+
* been seen in the live store. `write_stdin rejected: ` (:118) stays unsettled: its reason is the approval policy's.
|
|
163
177
|
*/
|
|
164
|
-
const SPAWN_ERROR = /^(?:execution error|exec_command failed|write_stdin(?: approval)? failed): /;
|
|
178
|
+
const SPAWN_ERROR = /^(?:execution error|exec_command failed|wait failed|write_stdin(?: approval)? failed): /;
|
|
165
179
|
/**
|
|
166
180
|
* A declined call is the user's refusal only where its message says so. Codex reports `ToolError::Rejected` as `declined` for a user's refusal and for some
|
|
167
181
|
* runtime rejections, and normalizes only the literal text `rejected by user` (events.rs:448-458): `exec command rejected by user` for a shell call (:456) and
|
|
@@ -189,15 +203,26 @@ const bodyText = (raw) => {
|
|
|
189
203
|
*/
|
|
190
204
|
const outcomeOf = (raw) => {
|
|
191
205
|
const body = bodyText(raw);
|
|
206
|
+
const printed = OUTPUT_MARK.exec(body);
|
|
207
|
+
const script = SCRIPT_HEAD.exec(printed === null ? body : body.slice(0, printed.index));
|
|
208
|
+
if (script !== null) {
|
|
209
|
+
const error = SCRIPT_ERROR.exec(body);
|
|
210
|
+
const output = printed === null ? '' : body.slice(printed.index + printed[0].length, error === null || error.index < printed.index ? undefined : error.index);
|
|
211
|
+
if (script[1] === 'failed') {
|
|
212
|
+
const message = error === null ? output : body.slice(error.index + error[0].length);
|
|
213
|
+
return { text: message.trim() === '' ? 'Script failed' : message, exit: undefined, spawnError: true, running: undefined, script: true };
|
|
214
|
+
}
|
|
215
|
+
return { text: output, exit: script[2] === undefined ? 0 : undefined, spawnError: false, running: script[2], script: true };
|
|
216
|
+
}
|
|
192
217
|
const wrapped = parse(body);
|
|
193
218
|
if (isObj(wrapped) && typeof wrapped.output === 'string')
|
|
194
|
-
return { text: wrapped.output, exit: isObj(wrapped.metadata) ? num(wrapped.metadata.exit_code) : undefined, spawnError: false, running: undefined };
|
|
219
|
+
return { text: wrapped.output, exit: isObj(wrapped.metadata) ? num(wrapped.metadata.exit_code) : undefined, spawnError: false, running: undefined, script: false };
|
|
195
220
|
const mark = OUTPUT_MARK.exec(body);
|
|
196
221
|
if (mark === null)
|
|
197
|
-
return { text: body, exit: undefined, spawnError: SPAWN_ERROR.test(body), running: undefined };
|
|
222
|
+
return { text: body, exit: undefined, spawnError: SPAWN_ERROR.test(body), running: undefined, script: false };
|
|
198
223
|
const head = body.slice(0, mark.index);
|
|
199
224
|
const code = EXIT_LINE.exec(head)?.[1];
|
|
200
|
-
return { text: body.slice(mark.index + mark[0].length), exit: code === undefined ? undefined : Number(code), spawnError: false, running: RUNNING_LINE.exec(head)?.[1] };
|
|
225
|
+
return { text: body.slice(mark.index + mark[0].length), exit: code === undefined ? undefined : Number(code), spawnError: false, running: RUNNING_LINE.exec(head)?.[1], script: false };
|
|
201
226
|
};
|
|
202
227
|
/**
|
|
203
228
|
* A failed call's error text. A negative code is no exit status: upstream writes -1 for a tool failure message (core/src/tools/events.rs:538-542) and for a
|
|
@@ -254,6 +279,8 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
254
279
|
let open;
|
|
255
280
|
let lastTotal;
|
|
256
281
|
const calls = new Map();
|
|
282
|
+
// The tools the session declared as dynamic (`dynamic_tools`): their output carries no failure signal, only their DynamicToolCall item does.
|
|
283
|
+
const dynamicNames = new Set();
|
|
257
284
|
// A unified-exec process still running, by its session id, to the exec_command that started it: in a Legacy rollout the `write_stdin` that sees it exit settles both.
|
|
258
285
|
const running = new Map();
|
|
259
286
|
// Records below this ordinal are the parent prefix a subagent's rollout copied (ordinal.rs:86-89), not the session's own. Only a Paginated rollout
|
|
@@ -267,7 +294,10 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
267
294
|
// one has none), so the leading records equal to the parent's, in order, are its copied history, in either mode. Null once it stops matching.
|
|
268
295
|
let copied = null;
|
|
269
296
|
const resolved = new Set();
|
|
297
|
+
// The stamp before the record being read, so an item's start (its stamp less its duration) is never earlier than the line before it.
|
|
298
|
+
let before;
|
|
270
299
|
const stamp = (value) => {
|
|
300
|
+
before = at;
|
|
271
301
|
const candidate = iso(value);
|
|
272
302
|
if (candidate !== undefined && (at === undefined || Date.parse(candidate) >= Date.parse(at)))
|
|
273
303
|
at = candidate;
|
|
@@ -286,15 +316,15 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
286
316
|
// path: a shell call's `[branch sha]` line is how a commit's sha is found. The output follows an `Exit code 0` line, the shape a Claude Code Bash
|
|
287
317
|
// result has (fixtures/proj/errors-a.jsonl:15): capture marks a result whose text starts `Exit code <nonzero>` as failed (`isErrorResult` in extract.ts), and a
|
|
288
318
|
// call that exited 0 may print such a line (`cat ci.log`). The commit patterns read every line (`COMMIT_LINE` and `HASH_LINE` in extract.ts, both `gm`), so the sha is still found.
|
|
289
|
-
const result = (id, failure, when, output = '') => {
|
|
319
|
+
const result = (id, failure, when, output = '', timed = false) => {
|
|
290
320
|
sequence += 1;
|
|
291
321
|
lines.push({
|
|
292
322
|
...base,
|
|
293
323
|
type: 'user',
|
|
294
324
|
uuid: `${fallbackId}-r${String(sequence)}`,
|
|
295
325
|
timestamp: when,
|
|
296
|
-
// Codex stamps a result with its own time, but the call's start is not paired to it, so no duration is claimed.
|
|
297
|
-
noDuration: true,
|
|
326
|
+
// Codex stamps a result with its own time, but the call's start is not paired to it, so no duration is claimed; a CommandExecution item's own `duration` is the exception.
|
|
327
|
+
...(timed ? {} : { noDuration: true }),
|
|
298
328
|
message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, is_error: failure !== undefined, content: failure ?? (output === '' ? '' : `Exit code 0\n${output}`) }] },
|
|
299
329
|
});
|
|
300
330
|
};
|
|
@@ -323,7 +353,8 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
323
353
|
};
|
|
324
354
|
// A shell call settles on its exit code, or on a failure to spawn, whether that comes from its own output or from a CommandExecution item. A
|
|
325
355
|
// process still running was remembered by its session id, so in a Legacy rollout the `write_stdin` that polled it and read its exit
|
|
326
|
-
// code settles both calls (in a Paginated one an item settles only its own exec_command
|
|
356
|
+
// code settles both calls (in a Paginated one an item settles only its own exec_command; a `wait` on a code-mode cell settles the cell in either mode, because
|
|
357
|
+
// the cell's outcome is its own header and no item carries it). A poll that failed instead (`write_stdin failed: ...`, `exited` false) says nothing about the process: a
|
|
327
358
|
// non-empty write to one with no tty is `StdinClosed` (unified_exec/process_manager.rs:1021-1026) and it runs on, so only the poll is the error.
|
|
328
359
|
const settleShell = (id, failure, output, when, exited = true) => {
|
|
329
360
|
const known = calls.get(id);
|
|
@@ -332,7 +363,7 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
332
363
|
resolved.add(id);
|
|
333
364
|
result(id, failure, when, output);
|
|
334
365
|
const pending = known.session === undefined ? undefined : running.get(known.session);
|
|
335
|
-
if (exited && !paginated && pending !== undefined && !resolved.has(pending)) {
|
|
366
|
+
if (exited && (!paginated || known.cell === true) && pending !== undefined && !resolved.has(pending)) {
|
|
336
367
|
resolved.add(pending);
|
|
337
368
|
result(pending, failure, when, output);
|
|
338
369
|
}
|
|
@@ -357,9 +388,35 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
357
388
|
else if (kind === 'FileChange' && id !== undefined && (item.status === 'completed' || item.status === 'failed' || (item.status === 'declined' && DECLINED_PATCH_TEXT.test(str(item.stderr) ?? '')))) {
|
|
358
389
|
settlePatch(id, item.status === 'declined' ? DECLINED_PATCH : item.status === 'failed' ? (str(item.stderr) ?? 'patch failed') : undefined, when);
|
|
359
390
|
}
|
|
360
|
-
else if (kind === 'CommandExecution' && id !== undefined && item.status === 'declined' &&
|
|
391
|
+
else if (kind === 'CommandExecution' && id !== undefined && item.status === 'declined' && calls.has(id)) {
|
|
361
392
|
// A `declined` one has exit code -1 (events.rs:552-554); only the user's refusal text settles it, as a refusal.
|
|
362
|
-
|
|
393
|
+
if (DECLINED_OUTPUT.test(str(item.aggregated_output) ?? ''))
|
|
394
|
+
settleShell(id, DECLINED_SHELL, '', when);
|
|
395
|
+
}
|
|
396
|
+
else if (kind === 'CommandExecution' && id !== undefined && (item.status === 'completed' || item.status === 'failed' || item.status === 'declined') && !calls.has(id)) {
|
|
397
|
+
// A code-mode cell's `exec_command` (Codex 0.166+) is run by the cell's JavaScript, so no call carries the item's `exec-<uuid>` id: the item is the
|
|
398
|
+
// command's only record, and it is its own Bash call. Binding it to its cell by position would hold for 191 of 193 items in a live 0.167.0 store and fail
|
|
399
|
+
// for 2, so it is not attempted.
|
|
400
|
+
const exit = num(item.exit_code);
|
|
401
|
+
if (resolved.has(id))
|
|
402
|
+
return;
|
|
403
|
+
resolved.add(id);
|
|
404
|
+
const output = str(item.aggregated_output) ?? '';
|
|
405
|
+
const spent = isObj(item.duration) ? Math.round((num(item.duration.secs) ?? 0) * 1000 + (num(item.duration.nanos) ?? 0) / 1e6) : undefined;
|
|
406
|
+
const parsed = Array.isArray(item.parsed_cmd) && item.parsed_cmd.length === 1 && isObj(item.parsed_cmd[0]) ? str(item.parsed_cmd[0].cmd) : undefined;
|
|
407
|
+
const joined = commandOf(item);
|
|
408
|
+
// The command array is the launcher then its flags then the text (`pwsh.exe -Command <text>`); a single parsed command is that text alone.
|
|
409
|
+
const command = parsed !== undefined && (joined === undefined || parsed.length < joined.length) ? parsed : joined;
|
|
410
|
+
const began = spent === undefined ? when : new Date(Math.max(Date.parse(when) - spent, Date.parse(before ?? when))).toISOString();
|
|
411
|
+
assistant([{ type: 'tool_use', id, name: 'Bash', native: 'exec_command', input: command === undefined ? {} : { command } }], began);
|
|
412
|
+
// An item with no exit_code (an Option, skipped when None, items.rs:284-286) keeps its command and has no outcome, as an owned one does.
|
|
413
|
+
// A refused one (exit code -1) is the user's refusal only where its output says so; any other declined text leaves no outcome.
|
|
414
|
+
if (item.status === 'declined') {
|
|
415
|
+
if (DECLINED_OUTPUT.test(output))
|
|
416
|
+
result(id, DECLINED_SHELL, when, '');
|
|
417
|
+
}
|
|
418
|
+
else if (exit !== undefined)
|
|
419
|
+
result(id, failureOf(exit, output), when, output, spent !== undefined);
|
|
363
420
|
}
|
|
364
421
|
else if (kind === 'CommandExecution' && id !== undefined && (item.status === 'completed' || item.status === 'failed')) {
|
|
365
422
|
// The exec_command's own item: `id` is its call id, written when the process exits, even one the model never polled (header). It carries the
|
|
@@ -375,6 +432,12 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
375
432
|
const text = isObj(item.error) ? (str(item.error.message) ?? '') : isObj(item.result) ? bodyText(item.result.content) : '';
|
|
376
433
|
settleMcp(id, failed ? (text === '' ? 'mcp tool call failed' : text) : undefined, when);
|
|
377
434
|
}
|
|
435
|
+
else if (kind === 'DynamicToolCall' && id !== undefined && (item.status === 'completed' || item.status === 'failed')) {
|
|
436
|
+
// The item's `success` and `error` (items.rs:301-322; dynamic.rs:246-262): a call with no item stays unsettled, since its output text says nothing of failure.
|
|
437
|
+
const failed = item.status === 'failed' || item.success === false;
|
|
438
|
+
const text = isObj(item.error) ? (str(item.error.message) ?? '') : '';
|
|
439
|
+
settleMcp(id, failed ? (text === '' ? 'dynamic tool call failed' : text) : undefined, when);
|
|
440
|
+
}
|
|
378
441
|
};
|
|
379
442
|
const parentKeys = (parent) => parent
|
|
380
443
|
.split('\n')
|
|
@@ -415,6 +478,8 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
415
478
|
const declared = toolNames(payload.dynamic_tools);
|
|
416
479
|
if (declared.length > 0)
|
|
417
480
|
tools({ offered: declared });
|
|
481
|
+
for (const name of declared)
|
|
482
|
+
dynamicNames.add(name);
|
|
418
483
|
inherited = num(payload.subagent_history_start_ordinal);
|
|
419
484
|
paginated = payload.history_mode === 'paginated';
|
|
420
485
|
if (inherited === undefined && parentText !== undefined && str(payload.forked_from_id) !== undefined && !isObj(payload.history_base))
|
|
@@ -443,10 +508,9 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
443
508
|
}
|
|
444
509
|
else if (kind === 'turn_aborted') {
|
|
445
510
|
// Only `interrupted` is the operator cutting a turn short: `replaced`, `review_ended`, and `budget_limited` are the harness's own (protocol.rs:4580-4585).
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
511
|
+
// It is written as the marker line Claude Code writes, so capture keeps it as a prompt record and the shape counts one interrupt (CAPTURE 23).
|
|
512
|
+
if (payload.reason === 'interrupted')
|
|
513
|
+
prompted(`${INTERRUPT_MARKER}]`, when);
|
|
450
514
|
}
|
|
451
515
|
else if (kind === 'agent_message') {
|
|
452
516
|
const message = str(payload.message);
|
|
@@ -527,9 +591,11 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
527
591
|
if (id === undefined)
|
|
528
592
|
continue;
|
|
529
593
|
if (kind === 'local_shell_call' || (namespace === undefined && SHELL_CALLS.has(name))) {
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
594
|
+
// A code-mode `exec` call's input is a JavaScript cell, not a command, so it has none.
|
|
595
|
+
const command = name === 'exec' ? undefined : commandOf(kind === 'local_shell_call' ? action : input);
|
|
596
|
+
const poll = name === 'wait' ? input.cell_id : input.session_id;
|
|
597
|
+
const polled = name === 'write_stdin' || name === 'wait' ? (str(poll) ?? (num(poll) === undefined ? undefined : String(num(poll)))) : undefined;
|
|
598
|
+
calls.set(id, { kind: 'shell', ids: [id], ...(polled === undefined ? {} : { session: name === 'wait' ? `cell:${polled}` : polled }), ...(name === 'exec' || name === 'wait' ? { cell: true } : {}) });
|
|
533
599
|
// `native` keeps Codex's own name for the action table (the contract at extract.ts, as cursor.ts does); a `local_shell_call` has no name field.
|
|
534
600
|
assistant([{ type: 'tool_use', id, name: 'Bash', native: kind === 'local_shell_call' ? 'local_shell' : name, input: command === undefined ? {} : { command } }], when);
|
|
535
601
|
}
|
|
@@ -540,7 +606,7 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
540
606
|
assistant(ids.map((part, index) => ({ type: 'tool_use', id: part, name: 'Edit', native: name, input: files[index] === undefined ? {} : { file_path: files[index] } })), when);
|
|
541
607
|
}
|
|
542
608
|
else {
|
|
543
|
-
calls.set(id, { kind: 'other', ids: [id] });
|
|
609
|
+
calls.set(id, { kind: 'other', ids: [id], ...(dynamicNames.has(name) ? { dynamic: true } : {}) });
|
|
544
610
|
const file = str(input.path) ?? str(input.file_path);
|
|
545
611
|
const query = str(input.query);
|
|
546
612
|
assistant([{ type: 'tool_use', id, name, input: { ...(file === undefined ? {} : { file_path: file }), ...(query === undefined ? {} : { query }) } }], when);
|
|
@@ -557,11 +623,11 @@ export const rolloutLines = (text, fallbackId, parentText) => {
|
|
|
557
623
|
if (out.exit === undefined && DECLINED_OUTPUT.test(out.text))
|
|
558
624
|
settleShell(id, DECLINED_SHELL, '', when, false);
|
|
559
625
|
else if (out.exit !== undefined || out.spawnError)
|
|
560
|
-
settleShell(id, out.spawnError ? out.text : failureOf(out.exit, out.text), out.text, when, out.exit !== undefined);
|
|
626
|
+
settleShell(id, out.spawnError ? out.text : failureOf(out.exit, out.text), out.text, when, out.exit !== undefined || out.script);
|
|
561
627
|
else if (out.running !== undefined && known.session === undefined)
|
|
562
|
-
running.set(out.running, id);
|
|
628
|
+
running.set(known.cell === true ? `cell:${out.running}` : out.running, id);
|
|
563
629
|
}
|
|
564
|
-
else if (known.kind === 'other') {
|
|
630
|
+
else if (known.kind === 'other' && known.dynamic !== true) {
|
|
565
631
|
// Another call's output carries no failure signal (an MCP call's comes from its mcp_tool_call_end), and a patch settles only from its
|
|
566
632
|
// patch_apply_end, because a rejected patch comes back as plain output text (tools/handlers/apply_patch.rs).
|
|
567
633
|
resolved.add(id);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Outcome } from '../cli.js';
|
|
2
2
|
import { type BooksBy } from './query.js';
|
|
3
|
+
import type { Emit } from '../telemetry/emit.js';
|
|
3
4
|
import { type PullInput } from './recall-reads.js';
|
|
4
5
|
import { type ContextOptions } from './hook-index.js';
|
|
5
6
|
import { type InstallRuntime, type Scope } from './install.js';
|
|
@@ -12,10 +13,13 @@ export interface SessionsDeps {
|
|
|
12
13
|
sessions: SessionsRuntime | undefined;
|
|
13
14
|
install: InstallRuntime | undefined;
|
|
14
15
|
now: () => number;
|
|
16
|
+
/** Records a telemetry event; absent in a run that has none. */
|
|
17
|
+
telemetry?: Emit | undefined;
|
|
15
18
|
}
|
|
16
19
|
export declare const sessionsDeposit: (deps: SessionsDeps, options: {
|
|
17
20
|
allow: string[];
|
|
18
21
|
allowFiles: string[];
|
|
22
|
+
deny?: string[];
|
|
19
23
|
redactDomains: string[];
|
|
20
24
|
}) => Promise<Outcome>;
|
|
21
25
|
/**
|
|
@@ -40,9 +44,10 @@ export declare const sessionsRead: (deps: SessionsDeps, options: {
|
|
|
40
44
|
export declare const sessionsContext: (deps: SessionsDeps, options: ContextOptions) => Promise<Outcome>;
|
|
41
45
|
export declare const sessionsMcp: (deps: SessionsDeps) => Promise<void>;
|
|
42
46
|
export declare const sessionsBooks: (deps: SessionsDeps, options: {
|
|
43
|
-
by: BooksBy;
|
|
47
|
+
by: BooksBy | "package";
|
|
44
48
|
sinceDays: number | undefined;
|
|
45
49
|
repo?: string;
|
|
50
|
+
termsFile?: string;
|
|
46
51
|
}) => Promise<Outcome>;
|
|
47
52
|
export declare const sessionsMeasure: (deps: SessionsDeps, options: {
|
|
48
53
|
last: number;
|
|
@@ -73,6 +78,11 @@ export interface VerifyReport {
|
|
|
73
78
|
selfBaseline: SelfBaselineResult[];
|
|
74
79
|
/** Batches in any server's outbox built under an older vocabulary, field list, or skeleton format: never printed, never sent, and removed by the next `pool deposit`. Counted from local folders; nothing is sent. */
|
|
75
80
|
staleBatches: number;
|
|
81
|
+
/** The outboxes' sent and masked counts (`pool-totals.ts`). */
|
|
82
|
+
sentBatches: number;
|
|
83
|
+
sentRaw: number;
|
|
84
|
+
rawCopies: number;
|
|
85
|
+
rawMasked: number;
|
|
76
86
|
}
|
|
77
87
|
/** With `strict`, any violated invariant, stuck step, `merkle: root mismatch`, or `merkle: counts do not bind` also fails the command (exit 4) after the full report is printed; the report is built first so callers can emit it. */
|
|
78
88
|
export declare const sessionsVerify: (deps: SessionsDeps, options: {
|
package/dist/sessions/command.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The `oars sessions` command group's bodies: each returns the `--json` document and the text a human sees.
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import { parseAllowList } from './allow.js';
|
|
4
|
+
import { denialOf, parseAllowList, runAllow } from './allow.js';
|
|
5
5
|
import { SOURCES, sourceLabel } from './sources.js';
|
|
6
6
|
import { CliError, ExitCode } from '../errors.js';
|
|
7
7
|
import { loadTerms } from './exclusion.js';
|
|
@@ -10,23 +10,30 @@ import { armsPath, coverageLines, formatScore, isGoldDocument, parseGold, plural
|
|
|
10
10
|
import { 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
|
+
import { recallFields } from '../telemetry/fields.js';
|
|
13
14
|
import { appendRecall, readRecalls } from './recall-object.js';
|
|
14
15
|
import { deriveConsequences, formatRecallCounts, recallCounts } from './recall-consequence.js';
|
|
15
16
|
import { pull, pullRefusal } from './recall-reads.js';
|
|
16
17
|
import { MISS_TEXT } from './recall-render.js';
|
|
17
18
|
import { readHookCalls } from './hook-calls.js';
|
|
18
19
|
import { buildIndex, contextBlock, inRepository, readIndex } from './hook-index.js';
|
|
19
|
-
import { hookStatus, installHooks, scheduleStatus, setSchedule, settingsPath, uninstallHooks } from './install.js';
|
|
20
|
+
import { ensureSchedule, hookStatus, installHooks, recordSchedule, SCHEDULE_LEFT_OFF, scheduleOptedOut, scheduleStatus, setSchedule, settingsPath, uninstallHooks } from './install.js';
|
|
20
21
|
import { serve } from './mcp.js';
|
|
21
22
|
import { refreshPool, servedPool } from './pool.js';
|
|
23
|
+
import { recallPool } from './recall-bundle.js';
|
|
24
|
+
import { booksLines, lastFetchedBundle } from './pool-books.js';
|
|
25
|
+
import { formatPackageBooks, packageBooks } from './package-books.js';
|
|
26
|
+
import { scanStore } from './exclusion.js';
|
|
27
|
+
import { vocabulary } from './vocab-load.js';
|
|
22
28
|
import { FAMILIES } from './read.js';
|
|
23
29
|
import { formatNotReadable, notReadable } from './read-readable.js';
|
|
24
30
|
import { formatReads, isFamily, parseSlice, selectReads } from './read-rows.js';
|
|
25
31
|
import { CAPTURE_VERSION, OLDEST_READABLE_CAPTURE } from './schema.js';
|
|
26
|
-
import { deposit, frozenSessions, loadStore, readCalls, readRedactDomains, readSkeletons, skippedFolders, slugOf } from './store.js';
|
|
32
|
+
import { deposit, frozenSessions, loadStore, readCalls, readRedactDomains, readSaved, readSkeletons, skippedFolders, slugOf } from './store.js';
|
|
27
33
|
import { formatStepCounters, inputKey, newContext, readSteps, stampKey, runDeposit, runStep, stepCounters, stuckSteps, tryLog } from './steps.js';
|
|
28
34
|
import { formatSelfBaseline, selfBaseline as selfBaselineOf } from './baseline.js';
|
|
29
35
|
import { staleBatchesAnywhere } from './pool-outbox.js';
|
|
36
|
+
import { outboxTotals } from './pool-totals.js';
|
|
30
37
|
import { auditCounts, auditStore, formatAudit } from './audit.js';
|
|
31
38
|
import { readSurvival, recordSurvival, SURVIVAL_DAYS } from './survival.js';
|
|
32
39
|
import { CAPTURE_INVARIANTS, formatSessionResults, formatVerify, verifyAll, verifyCaptured, verifySessions } from './verify.js';
|
|
@@ -35,24 +42,31 @@ const runtimeOf = (deps) => {
|
|
|
35
42
|
throw new CliError('The session store is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
36
43
|
return deps.sessions;
|
|
37
44
|
};
|
|
45
|
+
/** A list that names nothing is a mistake, not a request for the default (every folder): it is refused. */
|
|
38
46
|
const readAllowFile = async (file) => {
|
|
47
|
+
let entries;
|
|
39
48
|
try {
|
|
40
|
-
|
|
49
|
+
entries = parseAllowList(await readFile(file, 'utf8'));
|
|
41
50
|
}
|
|
42
51
|
catch {
|
|
43
52
|
throw new CliError(`Cannot read the --allow-file ${file}.`, ExitCode.USAGE, 'usage');
|
|
44
53
|
}
|
|
54
|
+
if (entries.length === 0)
|
|
55
|
+
throw new CliError(`The --allow-file ${file} names no folder. Without --allow or --allow-file, deposit reads every folder.`, ExitCode.USAGE, 'usage');
|
|
56
|
+
return entries;
|
|
45
57
|
};
|
|
46
58
|
export const sessionsDeposit = async (deps, options) => {
|
|
47
59
|
const fromFiles = await Promise.all(options.allowFiles.map((file) => readAllowFile(file)));
|
|
48
60
|
const runtime = runtimeOf(deps);
|
|
49
61
|
const table = runtime.sources ?? SOURCES;
|
|
50
|
-
const allow = [...options.allow, ...fromFiles.flat()];
|
|
51
|
-
const result = await runDeposit(newContext(runtime.storeDir, deps.now, 'manual'), true, inputKey('manual', allow, options.redactDomains), async (trace) => {
|
|
62
|
+
const allow = [...options.allow, ...fromFiles.flat(), ...(options.deny ?? []).map(denialOf)];
|
|
63
|
+
const { result, read } = await runDeposit(newContext(runtime.storeDir, deps.now, 'manual'), true, inputKey('manual', allow, options.redactDomains), async (trace) => {
|
|
64
|
+
// Survival reads the folders this deposit reads, from the list saved before the deposit merges this run's entries into it.
|
|
65
|
+
const scope = runAllow((await readSaved(runtime.storeDir)).allow, allow);
|
|
52
66
|
const done = await deposit(runtime, { allow, redactDomains: options.redactDomains, now: deps.now }, trace);
|
|
53
|
-
return { value: done, in: { transcript: done.sessions + done.subagents + done.unchanged } };
|
|
67
|
+
return { value: { result: done, read: scope }, in: { transcript: done.sessions + done.subagents + done.unchanged } };
|
|
54
68
|
});
|
|
55
|
-
const survival = await recordSurvival(runtime,
|
|
69
|
+
const survival = await recordSurvival(runtime, read, deps.now);
|
|
56
70
|
const lines = [
|
|
57
71
|
`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.`,
|
|
58
72
|
`Read ${String(result.folders.length)} folders; skipped ${String(result.skippedFolders)} not on the allowlist.`,
|
|
@@ -85,10 +99,12 @@ export const sessionsRecall = async (deps, query, sid = null) => {
|
|
|
85
99
|
const key = stampKey(JSON.stringify(query));
|
|
86
100
|
const result = await runStep(newContext(storeDir, deps.now, 'manual'), 'recall', async () => {
|
|
87
101
|
const store = await loadStore(storeDir);
|
|
88
|
-
const found = pull(store, query, { sid, now: deps.now(), frame: null, redactDomains: await readRedactDomains(storeDir), current: await
|
|
102
|
+
const found = pull(store, query, { sid, now: deps.now(), frame: null, redactDomains: await readRedactDomains(storeDir), current: await recallPool(storeDir, store, servedPool) });
|
|
89
103
|
return { value: found, report: { in: { query: 1 }, out: { matches: found.legacy?.matches.length ?? found.reads.length }, key } };
|
|
90
104
|
}, key);
|
|
91
105
|
await tryLog(() => appendRecall(storeDir, result.recall));
|
|
106
|
+
// 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
|
+
deps.telemetry?.('recall', recallFields(result.recall, result.reads.map((read) => read.family), result.legacy !== undefined));
|
|
92
108
|
if (result.legacy !== undefined)
|
|
93
109
|
return { document: { ok: true, ...result.legacy, recall: result.recall }, text: formatRecall(result.legacy) };
|
|
94
110
|
// 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.
|
|
@@ -141,7 +157,7 @@ export const sessionsContext = async (deps, options) => {
|
|
|
141
157
|
return { document: { ok: true, context: block ?? null }, text: block ?? 'Nothing recurs in this repository yet.' };
|
|
142
158
|
};
|
|
143
159
|
export const sessionsMcp = async (deps) => {
|
|
144
|
-
await serve(runtimeOf(deps), deps.now);
|
|
160
|
+
await serve(runtimeOf(deps), deps.now, deps.telemetry);
|
|
145
161
|
};
|
|
146
162
|
/** Names shown in the `verify` and `books` text; each `--json` document carries them all. */
|
|
147
163
|
const SKIPPED_NAMES_SHOWN = 10;
|
|
@@ -180,7 +196,23 @@ const inRepositoryStore = (store, repo) => {
|
|
|
180
196
|
newerCaptures: [...sessions, ...subagents].filter((record) => newer(record.cv)).length,
|
|
181
197
|
};
|
|
182
198
|
};
|
|
199
|
+
/**
|
|
200
|
+
* `books --by package`: the store's tool calls per public package, after the A38 scan `oars sessions exclusions` runs (a missing term file excludes everything).
|
|
201
|
+
* The scan reads the transcripts and writes nothing; the store is only read.
|
|
202
|
+
*/
|
|
203
|
+
const packageBooksOutcome = async (deps, options) => {
|
|
204
|
+
const runtime = runtimeOf(deps);
|
|
205
|
+
const [{ excluded }, store] = await Promise.all([scanStore(runtime, options.termsFile), loadStore(runtime.storeDir)]);
|
|
206
|
+
const cutoff = options.sinceDays === undefined ? undefined : new Date(deps.now() - options.sinceDays * 86_400_000).toISOString();
|
|
207
|
+
const result = packageBooks(store, new Set(excluded.map((entry) => entry.session)), vocabulary(), cutoff);
|
|
208
|
+
return { document: { ok: true, by: 'package', ...result, newerCaptures: store.newerCaptures }, text: [formatPackageBooks(result), ...newerNotice(store)].join('\n') };
|
|
209
|
+
};
|
|
183
210
|
export const sessionsBooks = async (deps, options) => {
|
|
211
|
+
if (options.by === 'package') {
|
|
212
|
+
if (options.repo !== undefined)
|
|
213
|
+
throw new CliError('--repo does not apply to --by package: its rows are public package names across the whole store.', ExitCode.USAGE, 'usage');
|
|
214
|
+
return packageBooksOutcome(deps, { sinceDays: options.sinceDays, termsFile: options.termsFile ?? '' });
|
|
215
|
+
}
|
|
184
216
|
const runtime = runtimeOf(deps);
|
|
185
217
|
const { storeDir } = runtime;
|
|
186
218
|
const loaded = await loadStore(storeDir);
|
|
@@ -194,7 +226,8 @@ export const sessionsBooks = async (deps, options) => {
|
|
|
194
226
|
? `No ${options.by === 'pull' ? 'pulls' : 'prompts'} in the --since window.`
|
|
195
227
|
: formatBooks(rows, options.by);
|
|
196
228
|
const scope = await scopeOf(runtime);
|
|
197
|
-
|
|
229
|
+
const bundle = await lastFetchedBundle(storeDir);
|
|
230
|
+
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') };
|
|
198
231
|
};
|
|
199
232
|
export const sessionsMeasure = async (deps, options) => {
|
|
200
233
|
const runtime = runtimeOf(deps);
|
|
@@ -240,7 +273,8 @@ export const sessionsVerify = async (deps, options) => {
|
|
|
240
273
|
const scope = await scopeOf(runtime);
|
|
241
274
|
// A local count like the scope: an outbox folder that cannot be read is no reason for `verify` to fail.
|
|
242
275
|
const staleBatches = await staleBatchesAnywhere(storeDir).catch(() => 0);
|
|
243
|
-
const
|
|
276
|
+
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 };
|
|
244
278
|
const failed = [...results.filter((result) => result.violations > 0).map((result) => result.name), ...(unreadable > 0 ? ['unreadable skeletons'] : []), ...merkleFailed, ...stuck.map((entry) => `stuck: ${entry.step}`)];
|
|
245
279
|
const stuckLines = stuck.map((entry) => `FAIL ${entry.step} is stuck: key ${entry.key} failed ${String(entry.runs)} runs in a row: ${entry.error}`);
|
|
246
280
|
const highWater = mine([...store.sessions, ...store.subagents]).map((session) => `${session.session}:${String(session.rev)}:${session.cv}`).sort();
|
|
@@ -249,7 +283,7 @@ export const sessionsVerify = async (deps, options) => {
|
|
|
249
283
|
failed,
|
|
250
284
|
report,
|
|
251
285
|
document: { ok: true, ...report, newerCaptures: store.newerCaptures },
|
|
252
|
-
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`] : []), ...newerNotice(store), ...(unreadable > 0 ? [`FAIL ${String(unreadable)} unreadable skeleton files (torn or corrupted); redeposit to rebuild them`] : []), ...stuckLines].join('\n'),
|
|
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'),
|
|
253
287
|
};
|
|
254
288
|
const cvBehind = results.find((result) => result.name === CAPTURE_INVARIANTS[0])?.violations ?? 0;
|
|
255
289
|
// The key is the sessions checked, each at its rev and cv, and the code's ev (spec 4.2).
|
|
@@ -339,19 +373,31 @@ const installOf = (deps) => {
|
|
|
339
373
|
throw new CliError('Hook install is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
340
374
|
return deps.install;
|
|
341
375
|
};
|
|
376
|
+
/** `--scope`, else `file` when `--file` names one, else the user settings. */
|
|
377
|
+
const hookScope = (options) => options.scope ?? (options.file === undefined ? 'user' : 'file');
|
|
342
378
|
export const sessionsHookInstall = async (deps, options) => {
|
|
343
379
|
const runtime = installOf(deps);
|
|
344
380
|
const lines = [];
|
|
345
381
|
const document = { ok: true };
|
|
346
|
-
// `--schedule` alone touches no settings file:
|
|
382
|
+
// `--schedule` alone touches no settings file: it names the task, not the hooks.
|
|
347
383
|
if (options.schedule === undefined || options.scope !== undefined || options.file !== undefined) {
|
|
348
|
-
const file = settingsPath(runtime, options
|
|
384
|
+
const file = settingsPath(runtime, hookScope(options), options.file);
|
|
349
385
|
const result = await installHooks(runtime, file, options.keepRetention);
|
|
350
386
|
document.install = result;
|
|
351
387
|
lines.push(`Installed ${result.installed.join(', ')} hooks in ${file}.`, result.retention);
|
|
352
388
|
}
|
|
353
|
-
|
|
354
|
-
|
|
389
|
+
// Without `--schedule`, Windows gets the nightly task too; `--schedule off` is the opt-out, and it sticks until `--schedule nightly`.
|
|
390
|
+
if (options.schedule !== undefined || runtime.platform === 'win32') {
|
|
391
|
+
const storeDir = deps.sessions?.storeDir;
|
|
392
|
+
const optedOut = options.schedule === undefined && storeDir !== undefined && (await scheduleOptedOut(storeDir));
|
|
393
|
+
let line = SCHEDULE_LEFT_OFF;
|
|
394
|
+
if (options.schedule !== undefined) {
|
|
395
|
+
line = await setSchedule(runtime, options.schedule);
|
|
396
|
+
await recordSchedule(storeDir, options.schedule);
|
|
397
|
+
}
|
|
398
|
+
else if (!optedOut) {
|
|
399
|
+
line = await ensureSchedule(runtime);
|
|
400
|
+
}
|
|
355
401
|
document.schedule = line;
|
|
356
402
|
lines.push(line);
|
|
357
403
|
}
|
|
@@ -359,7 +405,7 @@ export const sessionsHookInstall = async (deps, options) => {
|
|
|
359
405
|
};
|
|
360
406
|
export const sessionsHookUninstall = async (deps, options) => {
|
|
361
407
|
const runtime = installOf(deps);
|
|
362
|
-
const file = settingsPath(runtime, options
|
|
408
|
+
const file = settingsPath(runtime, hookScope(options), options.file);
|
|
363
409
|
const result = await uninstallHooks(file);
|
|
364
410
|
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).`;
|
|
365
411
|
return { document: { ok: true, ...result }, text };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Coverage, type OrgSize, type RepoVisibility, type Team } from './pool-wire.js';
|
|
2
|
+
import type { SessionObservation } from './schema.js';
|
|
3
|
+
import { type SessionsRuntime } from './store.js';
|
|
4
|
+
import { type Git } from './survival.js';
|
|
5
|
+
/** The two bands only the operator can state. */
|
|
6
|
+
export interface CoverageSetting {
|
|
7
|
+
orgSize: OrgSize;
|
|
8
|
+
repoVisibility: RepoVisibility;
|
|
9
|
+
}
|
|
10
|
+
export declare const coverageFile: (storeDir: string) => string;
|
|
11
|
+
/** The saved setting. A missing, damaged, or out-of-list file reads as `unknown`, which states nothing. */
|
|
12
|
+
export declare const readCoverageSetting: (storeDir: string) => Promise<CoverageSetting>;
|
|
13
|
+
export declare const writeCoverageSetting: (storeDir: string, setting: CoverageSetting) => Promise<void>;
|
|
14
|
+
/** The band for a count of distinct authors: one is `solo`, more is `team`, and none or an unread log is `unknown`. */
|
|
15
|
+
export declare const teamOf: (authors: number | null) => Team;
|
|
16
|
+
/** How many distinct commit authors the repository's local log shows (the author address, mailmap applied); null when git cannot read it or it holds no commit. Only the count leaves. */
|
|
17
|
+
export declare const authorsIn: (git: Git, root: string) => Promise<number | null>;
|
|
18
|
+
/** The coverage of a session, for one deposit pass: each repository's log is read once. */
|
|
19
|
+
export declare const makeCoverage: (runtime: Pick<SessionsRuntime, "projectsRoot" | "storeDir">, setting: CoverageSetting, git: Git) => ((main: SessionObservation) => Promise<Coverage>);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// What a deposit says about whom it covers (t871). Two of the bands are the operator's word, kept in one file on this machine and never looked up; the third, `team`, is a count of the distinct commit
|
|
2
|
+
// authors in the local git log of the repository the session sat in. The count is made here and dropped: the log is read, the authors are counted, and no name or address leaves this function. No call goes
|
|
3
|
+
// over a network, and anything that cannot be read goes as `unknown`.
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { makeRepoRoot } from './repo-root.js';
|
|
7
|
+
import { NO_COVERAGE, ORG_SIZES, REPO_VISIBILITIES } from './pool-wire.js';
|
|
8
|
+
import { writeAtomic } from './store.js';
|
|
9
|
+
import { cwdOfProject } from './survival.js';
|
|
10
|
+
/** A history this long is read at most, so a very large repository costs a bounded read; the band needs to tell one author from several, not to count them all. */
|
|
11
|
+
const LOG_COMMITS_MAX = 5000;
|
|
12
|
+
export const coverageFile = (storeDir) => join(storeDir, 'pool-coverage.json');
|
|
13
|
+
const held = (list, value, otherwise) => list.find((entry) => entry === value) ?? otherwise;
|
|
14
|
+
/** The saved setting. A missing, damaged, or out-of-list file reads as `unknown`, which states nothing. */
|
|
15
|
+
export const readCoverageSetting = async (storeDir) => {
|
|
16
|
+
const unset = { orgSize: NO_COVERAGE.orgSize, repoVisibility: NO_COVERAGE.repoVisibility };
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(await readFile(coverageFile(storeDir), 'utf8'));
|
|
19
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
20
|
+
return unset;
|
|
21
|
+
return { orgSize: held(ORG_SIZES, Reflect.get(parsed, 'orgSize'), unset.orgSize), repoVisibility: held(REPO_VISIBILITIES, Reflect.get(parsed, 'repoVisibility'), unset.repoVisibility) };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return unset;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export const writeCoverageSetting = (storeDir, setting) => writeAtomic(coverageFile(storeDir), `${JSON.stringify(setting)}\n`);
|
|
28
|
+
/** The band for a count of distinct authors: one is `solo`, more is `team`, and none or an unread log is `unknown`. */
|
|
29
|
+
export const teamOf = (authors) => (authors === null || authors === 0 ? 'unknown' : authors === 1 ? 'solo' : 'team');
|
|
30
|
+
/** How many distinct commit authors the repository's local log shows (the author address, mailmap applied); null when git cannot read it or it holds no commit. Only the count leaves. */
|
|
31
|
+
export const authorsIn = async (git, root) => {
|
|
32
|
+
try {
|
|
33
|
+
const lines = (await git(root, ['log', '--format=%aE', `--max-count=${String(LOG_COMMITS_MAX)}`])).split(/\r?\n/).filter((line) => line !== '');
|
|
34
|
+
return lines.length === 0 ? null : new Set(lines.map((line) => line.toLowerCase())).size;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** The coverage of a session, for one deposit pass: each repository's log is read once. */
|
|
41
|
+
export const makeCoverage = (runtime, setting, git) => {
|
|
42
|
+
const rootOf = makeRepoRoot();
|
|
43
|
+
const teams = new Map();
|
|
44
|
+
return async ({ project, session }) => {
|
|
45
|
+
const cwd = await cwdOfProject(runtime, project, new Set([session]));
|
|
46
|
+
const root = cwd === undefined ? null : rootOf(cwd);
|
|
47
|
+
if (root === null)
|
|
48
|
+
return { ...setting, team: 'unknown' };
|
|
49
|
+
const known = teams.get(root);
|
|
50
|
+
if (known !== undefined)
|
|
51
|
+
return { ...setting, team: known };
|
|
52
|
+
const team = teamOf(await authorsIn(git, root));
|
|
53
|
+
teams.set(root, team);
|
|
54
|
+
return { ...setting, team };
|
|
55
|
+
};
|
|
56
|
+
};
|
package/dist/sessions/cursor.js
CHANGED
|
@@ -217,9 +217,9 @@ const linesOf = (bubble, base, at, model) => {
|
|
|
217
217
|
const input = num(tokens.inputTokens) ?? 0;
|
|
218
218
|
const output = num(tokens.outputTokens) ?? 0;
|
|
219
219
|
const message = { role: 'assistant', id: bubble.id, content };
|
|
220
|
-
// A zero count means Cursor did not record one, so
|
|
220
|
+
// A zero count means Cursor did not record one, so that category is left out rather than written as a measured zero (and with neither, no usage is written).
|
|
221
221
|
if (input > 0 || output > 0)
|
|
222
|
-
message.usage = { input_tokens: input, output_tokens: output };
|
|
222
|
+
message.usage = { ...(input > 0 ? { input_tokens: input } : {}), ...(output > 0 ? { output_tokens: output } : {}) };
|
|
223
223
|
const lines = [{ ...base, type: 'assistant', uuid: bubble.id, requestId: bubble.id, timestamp: at, ...(model === undefined ? {} : { requestedModel: model }), message }];
|
|
224
224
|
if (call !== undefined && name !== undefined && (call.userDecision === 'rejected' || ((call.status === 'error' || call.status === 'completed') && !aborted(call, name)))) {
|
|
225
225
|
const failure = failureOf(call, name);
|