@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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// The next-node read (t926): what the agent is shown at the moments before it decides. Two reads, both from cells and never from text: the recovery edge (what followed this
|
|
2
|
+
// failure, for this action and error class) and the split positions (the places in a repo's earlier work where the next step is not settled). A read is built from the kept
|
|
3
|
+
// bundle for the session's setup first and the local pool's reads second, and every shown or held one is a `Recall` row, so the holdback arms are real.
|
|
4
|
+
import { CANONICAL_ACTIONS } from './actions.js';
|
|
5
|
+
import { harnessBucket } from './cells.js';
|
|
6
|
+
import { appendRecall, armOf, makeRecall, parseRecalls, recallsPath } from './recall-object.js';
|
|
7
|
+
import { frameOfTail, tailOf } from './recall-push.js';
|
|
8
|
+
import { sessionBundle } from './pool-replica.js';
|
|
9
|
+
import { readIdOf } from './recall-reads.js';
|
|
10
|
+
import { closedName, count, evidenceOf } from './recall-render.js';
|
|
11
|
+
import { LITERAL } from './public-vocabulary.js';
|
|
12
|
+
/**
|
|
13
|
+
* The count a recovery edge needs before it is served from the local pool. The pool's own served floor is `K_CELL` (5 accounts, `packages/pool/src/derive.ts`); a bundle's read has
|
|
14
|
+
* cleared it already, so only a local edge is held to this, counted in followed steps because one operator's store is one account.
|
|
15
|
+
*/
|
|
16
|
+
export const RECOVERY_FLOOR = 5;
|
|
17
|
+
/** The steps a position needs behind it before it can be called split: t782's qualifying cell is a cell topped up to 20 sessions. */
|
|
18
|
+
export const POSITION_MIN_N = 20;
|
|
19
|
+
/** The band of t782 plan 1.7: the leading option's share, 20 to 80 percent inclusive. A settled position (above) and a diffuse one (below) are not listed. */
|
|
20
|
+
export const POSITION_BAND = { low: 0.2, high: 0.8 };
|
|
21
|
+
/**
|
|
22
|
+
* The bytes a table may add to a turn's context. Eight lines of about 70 bytes: a session start or a prompt carries it every time it is sent, and a table the agent has to
|
|
23
|
+
* read past is one it stops reading. Bundle positions come first, then local ones, each by observations; the cap cuts from the end, so a local position goes before a bundle one.
|
|
24
|
+
*/
|
|
25
|
+
export const TABLE_CAP_BYTES = 600;
|
|
26
|
+
const TOP_NEXT = 3;
|
|
27
|
+
const MAX_FOLLOWED = 3;
|
|
28
|
+
const isCount = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
29
|
+
const isAction = (name) => CANONICAL_ACTIONS.some((action) => action === name);
|
|
30
|
+
const byWeight = (a, b) => b.followed - a.followed || a.next.localeCompare(b.next);
|
|
31
|
+
/**
|
|
32
|
+
* The recovery edges of a set of reads, by `action/errorCode`, each next step summed over its weeks. A next step that is not a canonical action (or `none`) is left out, so the
|
|
33
|
+
* names a hook shows are the closed ones whatever a store holds.
|
|
34
|
+
*/
|
|
35
|
+
export const recoveryEdgesOf = (reads, keep = () => true) => {
|
|
36
|
+
const grouped = new Map();
|
|
37
|
+
// The failures of one action and error class, once: every week's reads of its next steps share a week's failures, so a next step seen in fewer weeks must not get a smaller denominator.
|
|
38
|
+
// A week's failures are the sum of what followed them, so the total over every next step is that sum over all its weeks.
|
|
39
|
+
const failures = new Map();
|
|
40
|
+
for (const read of reads) {
|
|
41
|
+
if (read.family !== 'recovery')
|
|
42
|
+
continue;
|
|
43
|
+
const { action, errorCode, next } = read.slice;
|
|
44
|
+
const { followed, of } = read.value;
|
|
45
|
+
if (action === undefined || errorCode === undefined || next === undefined || !isCount(followed) || !isCount(of) || followed === 0)
|
|
46
|
+
continue;
|
|
47
|
+
const key = `${action}/${errorCode}`;
|
|
48
|
+
failures.set(key, (failures.get(key) ?? 0) + followed);
|
|
49
|
+
if (!(next === 'none' || isAction(next)))
|
|
50
|
+
continue;
|
|
51
|
+
const edges = grouped.get(key) ?? new Map();
|
|
52
|
+
grouped.set(key, edges);
|
|
53
|
+
const have = edges.get(next);
|
|
54
|
+
if (have === undefined) {
|
|
55
|
+
edges.set(next, { edge: { next, followed, of, clusters: read.clusters, readId: read.readId, day: read.asOf.day }, heaviest: followed });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
have.edge.followed += followed;
|
|
59
|
+
have.edge.clusters = Math.max(have.edge.clusters, read.clusters);
|
|
60
|
+
if (followed > have.heaviest) {
|
|
61
|
+
have.heaviest = followed;
|
|
62
|
+
have.edge.readId = read.readId;
|
|
63
|
+
}
|
|
64
|
+
if (read.asOf.day !== null && (have.edge.day === null || read.asOf.day > have.edge.day))
|
|
65
|
+
have.edge.day = read.asOf.day;
|
|
66
|
+
}
|
|
67
|
+
const edges = {};
|
|
68
|
+
for (const [key, byNext] of grouped) {
|
|
69
|
+
const sorted = [...byNext.values()].map((weighted) => ({ ...weighted.edge, of: failures.get(key) ?? weighted.edge.of })).sort(byWeight);
|
|
70
|
+
if (sorted[0] !== undefined && keep(sorted[0].followed))
|
|
71
|
+
edges[key] = sorted.slice(0, MAX_FOLLOWED);
|
|
72
|
+
}
|
|
73
|
+
return edges;
|
|
74
|
+
};
|
|
75
|
+
/** True when a position's prefix names only canonical actions (no identity, no class): the table prints names and numbers and nothing a store wrote. */
|
|
76
|
+
const closedPrefix = (prefix) => prefix.split('>').every(isAction);
|
|
77
|
+
/** The positions of a set of reads where the next step is split by t782's rule: enough observations, and the leading step's share inside the band. */
|
|
78
|
+
export const positionsOf = (reads) => {
|
|
79
|
+
const positions = [];
|
|
80
|
+
for (const read of reads) {
|
|
81
|
+
if (read.family !== 'next-step')
|
|
82
|
+
continue;
|
|
83
|
+
const { prefix } = read.slice;
|
|
84
|
+
const { next, topShare } = read.value;
|
|
85
|
+
if (prefix === undefined || !closedPrefix(prefix) || !isCount(topShare) || topShare < POSITION_BAND.low || topShare > POSITION_BAND.high || read.n < POSITION_MIN_N)
|
|
86
|
+
continue;
|
|
87
|
+
if (typeof next !== 'object' || next === null)
|
|
88
|
+
continue;
|
|
89
|
+
const ranked = Object.entries(next)
|
|
90
|
+
.filter(([action, times]) => isAction(action) && isCount(times))
|
|
91
|
+
.sort(([a, one], [b, two]) => two - one || a.localeCompare(b))
|
|
92
|
+
.slice(0, TOP_NEXT);
|
|
93
|
+
if (ranked.length < 2)
|
|
94
|
+
continue;
|
|
95
|
+
positions.push({ prefix, n: read.n, next: ranked, clusters: read.clusters, readId: read.readId, day: read.asOf.day });
|
|
96
|
+
}
|
|
97
|
+
return positions.sort((a, b) => b.n - a.n || a.prefix.localeCompare(b.prefix));
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* What a failure is answered with: the bundle's edges for this action and error class if it holds any (they cleared the pool's floor), else the local pool's. Silent when the top
|
|
101
|
+
* next step is a same-step retry (direction 13c), when a local edge is under `RECOVERY_FLOOR`, or when the top is that nothing followed (`ended`).
|
|
102
|
+
*/
|
|
103
|
+
export const recoveryDecision = (bundle, local, action, code) => {
|
|
104
|
+
const key = `${action}/${code}`;
|
|
105
|
+
const fromBundle = bundle?.recovery[key];
|
|
106
|
+
const source = fromBundle === undefined || bundle === undefined ? undefined : { edges: fromBundle, source: 'bundle', poolVersion: bundle.poolVersion };
|
|
107
|
+
const fromLocal = local?.recovery[key];
|
|
108
|
+
const chosen = source ?? (fromLocal === undefined || local === undefined ? undefined : { edges: fromLocal, source: 'local', poolVersion: local.poolVersion });
|
|
109
|
+
const top = chosen?.edges[0];
|
|
110
|
+
if (chosen === undefined || top === undefined)
|
|
111
|
+
return { kind: 'none' };
|
|
112
|
+
// A tie with another next step is not a retry-only cell, so it is served; the strict lead is what direction 13c silences.
|
|
113
|
+
const second = chosen.edges[1];
|
|
114
|
+
if (top.next === action && (second === undefined || top.followed > second.followed))
|
|
115
|
+
return { kind: 'retry' };
|
|
116
|
+
if (top.next === 'none')
|
|
117
|
+
return { kind: 'ended' };
|
|
118
|
+
if (chosen.source === 'local' && top.followed < RECOVERY_FLOOR)
|
|
119
|
+
return { kind: 'floor' };
|
|
120
|
+
return { kind: 'serve', ...chosen };
|
|
121
|
+
};
|
|
122
|
+
const percent = (part, whole) => `${String(Math.round((part / whole) * 100))}%`;
|
|
123
|
+
/** The recovery read as text: names (closed) and numbers. */
|
|
124
|
+
export const renderRecovery = (action, code, edges) => {
|
|
125
|
+
const shown = edges.filter((edge) => closedName(edge.next) !== LITERAL);
|
|
126
|
+
const total = shown[0]?.of ?? 0;
|
|
127
|
+
const lines = shown.map((edge) => `${edge.next} x${String(edge.followed)}${total > 0 ? ` (${percent(edge.followed, total)})` : ''}`);
|
|
128
|
+
const rendered = [`oars recall, counts from earlier sessions: after a failed ${action} (${code})${total > 0 ? ` in ${count(total, 'failure')}` : ''}, the next step was`, ` ${lines.join(', ')}`, ` read ${shown[0]?.readId ?? ''} (recovery)`].join('\n');
|
|
129
|
+
return { rendered, names: shown.map((edge) => edge.next) };
|
|
130
|
+
};
|
|
131
|
+
/** The positions table, with every position that fits under the cap and nothing when none does. */
|
|
132
|
+
export const renderPositions = (positions) => {
|
|
133
|
+
const header = 'oars recall, next steps that earlier sessions split on (counts, not advice):';
|
|
134
|
+
const listed = [];
|
|
135
|
+
let used = Buffer.byteLength(header);
|
|
136
|
+
const lines = [];
|
|
137
|
+
for (const position of positions) {
|
|
138
|
+
const line = `- after ${position.prefix}: ${position.next.map(([action, times]) => `${action} ${percent(times, position.n)}`).join(', ')} (n ${String(position.n)})`;
|
|
139
|
+
const size = Buffer.byteLength(line) + 1;
|
|
140
|
+
if (used + size > TABLE_CAP_BYTES)
|
|
141
|
+
break;
|
|
142
|
+
used += size;
|
|
143
|
+
listed.push(position);
|
|
144
|
+
lines.push(line);
|
|
145
|
+
}
|
|
146
|
+
return { rendered: listed.length === 0 ? '' : [header, ...lines].join('\n'), listed, names: [...new Set(listed.flatMap((position) => position.next.map(([action]) => action)))] };
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* The key a hook index files the local pool's reads under for a session: its harness and the bucket of the version it runs (`recall-frames.ts`). A session whose version is not
|
|
150
|
+
* stated is read across every version of its harness, which is the filter that can be applied honestly.
|
|
151
|
+
*/
|
|
152
|
+
export const frameKeyOf = (asker) => `${asker.harness}/${asker.harnessVersion === null ? '*' : harnessBucket(asker.harnessVersion)}`;
|
|
153
|
+
/** The bundle's and the local pool's positions as one list, a prefix once: the bundle's positions by observations, then the local ones it lacks by observations. */
|
|
154
|
+
export const mergePositions = (bundle, local) => {
|
|
155
|
+
const byObservations = (a, b) => b.n - a.n || a.prefix.localeCompare(b.prefix);
|
|
156
|
+
const seen = new Set(bundle.map((position) => position.prefix));
|
|
157
|
+
return [...[...bundle].sort(byObservations), ...local.filter((position) => !seen.has(position.prefix)).sort(byObservations)];
|
|
158
|
+
};
|
|
159
|
+
/** A read as the evidence record wants it: counts and closed names only. */
|
|
160
|
+
const evidenceRead = (read, setup) => ({
|
|
161
|
+
readId: read.readId,
|
|
162
|
+
family: read.family,
|
|
163
|
+
tool: read.tool,
|
|
164
|
+
keys: [],
|
|
165
|
+
n: read.n,
|
|
166
|
+
count: read.count,
|
|
167
|
+
calls: read.calls,
|
|
168
|
+
clusters: read.clusters,
|
|
169
|
+
firstDay: null,
|
|
170
|
+
lastDay: read.day,
|
|
171
|
+
harnesses: [setup.harness],
|
|
172
|
+
harnessVersions: setup.harnessVersion === null ? [] : [setup.harnessVersion],
|
|
173
|
+
models: [],
|
|
174
|
+
followed: read.followed,
|
|
175
|
+
});
|
|
176
|
+
/** The asker's frame, from the tail of its transcript. */
|
|
177
|
+
export const frameFor = (input) => frameOfTail(input.tail, input.platform, input.at);
|
|
178
|
+
/** The reads of the kept bundle for the asker's harness and version; undefined with no version stated, no bundle, or a bundle that does not verify (never an error). */
|
|
179
|
+
export const bundleNext = async (storeDir, asker) => {
|
|
180
|
+
const bundle = asker.harnessVersion === null ? undefined : await sessionBundle(storeDir, { harness: asker.harness, harnessVersion: asker.harnessVersion });
|
|
181
|
+
return bundle === undefined ? undefined : { poolVersion: bundle.asOf.pool, recovery: recoveryEdgesOf(bundle.reads), positions: positionsOf(bundle.reads) };
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* A served or held read: the row is written before anything is injected, and the arm is the session's for this query against this pool. The row is the recall's exposure record
|
|
185
|
+
* (ids and counts, and the rendered text a held read would have shown); an arm that kept it back is returned so the hook injects nothing.
|
|
186
|
+
*/
|
|
187
|
+
export const recordRead = async (input, frame, query, armPrefix, poolVersion, rendered, names, reads) => {
|
|
188
|
+
const recall = makeRecall({
|
|
189
|
+
sid: input.sid,
|
|
190
|
+
at: input.at,
|
|
191
|
+
frame,
|
|
192
|
+
mode: 'push',
|
|
193
|
+
query,
|
|
194
|
+
poolVersion,
|
|
195
|
+
arm: armOf(input.sid, armPrefix, poolVersion),
|
|
196
|
+
reads: reads.map((read) => read.readId),
|
|
197
|
+
rendered,
|
|
198
|
+
names,
|
|
199
|
+
evidence: reads.map((read) => evidenceOf(read, frame)),
|
|
200
|
+
});
|
|
201
|
+
await appendRecall(input.storeDir, recall);
|
|
202
|
+
return recall;
|
|
203
|
+
};
|
|
204
|
+
/** The recovery read for one failure, as a recall row. */
|
|
205
|
+
export const recoveryRecall = (input, setup, action, code, decision) => {
|
|
206
|
+
const frame = frameFor(input);
|
|
207
|
+
const { rendered, names } = renderRecovery(action, code, decision.edges);
|
|
208
|
+
const top = decision.edges[0];
|
|
209
|
+
const read = evidenceRead({ readId: top?.readId ?? '', family: 'recovery', tool: action, n: top?.followed ?? 0, count: top?.followed ?? 0, calls: top?.of ?? null, clusters: top?.clusters ?? 1, day: top?.day ?? null, followed: decision.edges.map((edge) => [edge.next, edge.followed]) }, setup);
|
|
210
|
+
return recordRead(input, frame, { form: 'question', family: 'recovery', prefix: [`${action}/${code}`] }, [`${action}/${code}`], decision.poolVersion, rendered, names, [read]);
|
|
211
|
+
};
|
|
212
|
+
/** The old signature line's recall: the local signature index, held to the same retry rule and floor. */
|
|
213
|
+
export const signatureRecall = (input, setup, tool, sig, found, poolVersion) => {
|
|
214
|
+
const frame = frameFor(input);
|
|
215
|
+
const followed = found.followed.map(([next]) => next).join(', ');
|
|
216
|
+
const rendered = `oars recall: seen in ${String(found.sessions)} sessions; what followed: ${followed === '' ? 'nothing recorded' : followed}`;
|
|
217
|
+
const read = evidenceRead({ readId: readIdOf(sig), family: 'recovery', tool, n: found.sessions, count: found.sessions, calls: null, clusters: 1, day: null, followed: found.followed }, setup);
|
|
218
|
+
return recordRead(input, frame, { form: 'question', family: 'recovery', prefix: [readIdOf(sig)] }, [readIdOf(sig)], poolVersion, rendered, found.followed.map(([next]) => next), [read]);
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* The signature line is a same-step retry when the tool that failed is the one that most often followed, strictly: a tie with another tool is not a retry-only cell. It keeps its
|
|
222
|
+
* own floor, the index's two sessions: it states how many sessions saw the error, so a thin count is printed as thin, which a cell's share is not.
|
|
223
|
+
*/
|
|
224
|
+
export const isSignatureRetry = (tool, found) => {
|
|
225
|
+
const [first, second] = found.followed;
|
|
226
|
+
return first?.[0] === tool && (second === undefined || first[1] > second[1]);
|
|
227
|
+
};
|
|
228
|
+
/** The positions table for a session, as a recall row; undefined when no position qualifies or the table is the one `previous` already sent. */
|
|
229
|
+
export const positionsRecall = (input, setup, positions, poolVersion, previous) => {
|
|
230
|
+
const { rendered, listed, names } = renderPositions(positions);
|
|
231
|
+
if (listed.length === 0 || rendered === previous)
|
|
232
|
+
return Promise.resolve(undefined);
|
|
233
|
+
const frame = frameFor(input);
|
|
234
|
+
const reads = listed.map((position) => evidenceRead({ readId: position.readId, family: 'next-step', tool: position.next[0]?.[0] ?? '', n: position.n, count: position.next[0]?.[1] ?? 0, calls: position.n, clusters: position.clusters, day: position.day, followed: position.next }, setup));
|
|
235
|
+
return recordRead(input, frame, { form: 'question', family: 'next-step', prefix: ['positions'] }, ['positions'], poolVersion, rendered, names, reads);
|
|
236
|
+
};
|
|
237
|
+
/** The text the session was last sent a table with, from `recalls.jsonl`; undefined when it was never sent one. */
|
|
238
|
+
export const lastTable = async (storeDir, sid) => {
|
|
239
|
+
const mine = parseRecalls(await tailOf(recallsPath(storeDir))).filter((recall) => recall.mode === 'push' && recall.sid === sid && recall.query.family === 'next-step');
|
|
240
|
+
return mine.at(-1)?.answer.rendered;
|
|
241
|
+
};
|
|
@@ -34,6 +34,8 @@ export interface RecallAnswer {
|
|
|
34
34
|
reads: string[];
|
|
35
35
|
/** The size of what was injected, or of what a held recall would have injected. */
|
|
36
36
|
tokens: number;
|
|
37
|
+
/** The UTF-8 bytes of `rendered`: what a hook injected, or would have. Absent on rows written before it was recorded. */
|
|
38
|
+
bytes?: number;
|
|
37
39
|
/** What the asker saw, or for a held recall would have seen. It holds structured fields only (`recall-render.ts`). */
|
|
38
40
|
rendered: string;
|
|
39
41
|
/** The closed names a follow can match: canonical actions, in the order shown. */
|
|
@@ -28,6 +28,7 @@ export const makeRecall = (input) => ({
|
|
|
28
28
|
kind: input.reads.length === 0 ? 'miss' : 'hit',
|
|
29
29
|
reads: [...input.reads],
|
|
30
30
|
tokens: Math.ceil(input.rendered.length / CHARS_PER_TOKEN),
|
|
31
|
+
bytes: Buffer.byteLength(input.rendered),
|
|
31
32
|
rendered: input.rendered,
|
|
32
33
|
names: [...input.names],
|
|
33
34
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { HookIndex } from './hook-index.js';
|
|
2
2
|
import { type Recall } from './recall-object.js';
|
|
3
3
|
import { type Frame } from './schema.js';
|
|
4
|
-
|
|
5
|
-
export
|
|
4
|
+
import { tailOf } from './transcript-tail.js';
|
|
5
|
+
export { tailOf };
|
|
6
6
|
/**
|
|
7
7
|
* The asker's frame, as far as a transcript's tail states it. The stack and the shell need the whole transcript, which a hook has no time to read, so they stay
|
|
8
8
|
* unknown here; `delta` says unknown for them, and derive reads the session's real frame.
|
|
@@ -1,66 +1,18 @@
|
|
|
1
1
|
// The push: a pre-tool hook's recall (spec Section 2.7). The harness tells the hook only the call it is about to run, so the query is the call's shape, and a shape
|
|
2
2
|
// with a read over the floors and `PUSH_MIN_LIFT` is the one thing that can arm a push. A held push stores what it would have shown and injects nothing, which is the unexposed baseline.
|
|
3
|
-
import { open } from 'node:fs/promises';
|
|
4
3
|
import { closedFrame, closedStack, utcDay } from './frame.js';
|
|
5
4
|
import { appendRecall, armOf, makeRecall, parseRecalls, recallsPath } from './recall-object.js';
|
|
6
5
|
import { toolShapeId } from './recall-reads.js';
|
|
7
6
|
import { evidenceOf, renderAnswer } from './recall-render.js';
|
|
8
7
|
import { harnessOf } from './schema.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const isMissing = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
12
|
-
const openIfThere = async (file) => {
|
|
13
|
-
try {
|
|
14
|
-
return await open(file, 'r');
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
if (isMissing(error))
|
|
18
|
-
return undefined;
|
|
19
|
-
throw error;
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
/** A file's last bytes; a first line this cuts in two fails to parse and is skipped by whoever reads it. */
|
|
23
|
-
export const tailOf = async (file) => {
|
|
24
|
-
const handle = await openIfThere(file);
|
|
25
|
-
if (handle === undefined)
|
|
26
|
-
return '';
|
|
27
|
-
try {
|
|
28
|
-
const { size } = await handle.stat();
|
|
29
|
-
const length = Math.min(size, TAIL_BYTES);
|
|
30
|
-
const buffer = Buffer.alloc(length);
|
|
31
|
-
await handle.read(buffer, 0, length, size - length);
|
|
32
|
-
return buffer.toString('utf8');
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
await handle.close();
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
39
|
-
const text = (value) => (typeof value === 'string' && value !== '' ? value : null);
|
|
40
|
-
const parsedLines = (tail) => tail.split('\n').flatMap((line) => {
|
|
41
|
-
try {
|
|
42
|
-
const parsed = JSON.parse(line);
|
|
43
|
-
return isRecord(parsed) ? [parsed] : [];
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
});
|
|
8
|
+
import { statedOf, tailOf } from './transcript-tail.js';
|
|
9
|
+
export { tailOf };
|
|
49
10
|
/**
|
|
50
11
|
* The asker's frame, as far as a transcript's tail states it. The stack and the shell need the whole transcript, which a hook has no time to read, so they stay
|
|
51
12
|
* unknown here; `delta` says unknown for them, and derive reads the session's real frame.
|
|
52
13
|
*/
|
|
53
14
|
export const frameOfTail = (tail, platform, at) => {
|
|
54
|
-
|
|
55
|
-
let model = null;
|
|
56
|
-
let effort = null;
|
|
57
|
-
// Oldest first, so the newest line that states a field wins.
|
|
58
|
-
for (const line of parsedLines(tail)) {
|
|
59
|
-
harnessVersion = text(line.version) ?? harnessVersion;
|
|
60
|
-
effort = text(line.effort) ?? effort;
|
|
61
|
-
if (isRecord(line.message))
|
|
62
|
-
model = text(line.message.model) ?? model;
|
|
63
|
-
}
|
|
15
|
+
const { harnessVersion, model, effort } = statedOf(tail);
|
|
64
16
|
return closedFrame({ harness: harnessOf({}), harnessVersion, model, effort, day: utcDay(at), platform, shell: null, stack: closedStack({ languages: [], frameworks: [], size: null }) });
|
|
65
17
|
};
|
|
66
18
|
/**
|
|
@@ -46,6 +46,13 @@ export interface FitFloors {
|
|
|
46
46
|
* calls that always fail (`Monitor` with `query`, `Read` with `path`), and leave Bash, whose failures do not depend on its shape. Open parameters.
|
|
47
47
|
*/
|
|
48
48
|
export declare const FIT_FLOORS: FitFloors;
|
|
49
|
+
/**
|
|
50
|
+
* The floors the pre-tool push holds a shape to (t926). The pre-tool hook fires after the call is decided, so a push is a warning or nothing: a shape that fails less than half
|
|
51
|
+
* its calls is a base rate (the two broad shell shapes, `Bash` and `PowerShell` with a command, fail about a third of theirs whatever the command) and warning on it tells the
|
|
52
|
+
* agent nothing it can act on. A shape that fails at least half its calls is a defect of the call itself (`Monitor` with `query`, `Read` with `path`). Pulls and the pool keep
|
|
53
|
+
* `FIT_FLOORS`: a base rate is still a fact to read on request.
|
|
54
|
+
*/
|
|
55
|
+
export declare const PUSH_FLOORS: FitFloors;
|
|
49
56
|
/** `keep` narrows which calls are tallied, not what can come next: the call after a kept one is what followed it, wherever the slice ends. */
|
|
50
57
|
export declare const fitReads: (store: Store, floors?: FitFloors, keep?: (call: ToolObservation) => boolean) => RecallRead[];
|
|
51
58
|
/** The action of a tool under the first of its harnesses whose table lists it, else under the first harness (the default one when a read names none). */
|
|
@@ -102,6 +102,13 @@ export const callsBySession = (store) => {
|
|
|
102
102
|
* calls that always fail (`Monitor` with `query`, `Read` with `path`), and leave Bash, whose failures do not depend on its shape. Open parameters.
|
|
103
103
|
*/
|
|
104
104
|
export const FIT_FLOORS = { minFailedSessions: 3, minFailedShare: 0.1, minCalls: 20 };
|
|
105
|
+
/**
|
|
106
|
+
* The floors the pre-tool push holds a shape to (t926). The pre-tool hook fires after the call is decided, so a push is a warning or nothing: a shape that fails less than half
|
|
107
|
+
* its calls is a base rate (the two broad shell shapes, `Bash` and `PowerShell` with a command, fail about a third of theirs whatever the command) and warning on it tells the
|
|
108
|
+
* agent nothing it can act on. A shape that fails at least half its calls is a defect of the call itself (`Monitor` with `query`, `Read` with `path`). Pulls and the pool keep
|
|
109
|
+
* `FIT_FLOORS`: a base rate is still a fact to read on request.
|
|
110
|
+
*/
|
|
111
|
+
export const PUSH_FLOORS = { ...FIT_FLOORS, minFailedShare: 0.5 };
|
|
105
112
|
/** `keep` narrows which calls are tallied, not what can come next: the call after a kept one is what followed it, wherever the slice ends. */
|
|
106
113
|
export const fitReads = (store, floors = FIT_FLOORS, keep = () => true) => {
|
|
107
114
|
const models = modelsByPrompt(store);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CanonicalAction } from './actions.js';
|
|
2
|
+
import type { ToolObservation } from './schema.js';
|
|
3
|
+
/** What the scan reads of a call, so a stored call and a skeleton's call both fit: its run, prompt, status, and the action of its first step. */
|
|
4
|
+
export type Followed = Pick<ToolObservation, 'side' | 'prompt'> & {
|
|
5
|
+
status: ToolObservation['status'] | 'other';
|
|
6
|
+
steps?: readonly {
|
|
7
|
+
action: CanonicalAction;
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
/** How many following calls a failed call lists. */
|
|
11
|
+
export declare const RECOVERY_CALLS = 5;
|
|
12
|
+
/** What followed one failed call: the actions of the next calls, and whether any of them wrote a file. */
|
|
13
|
+
export interface RecoveryCalls {
|
|
14
|
+
next: CanonicalAction[];
|
|
15
|
+
editFollowed: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* What followed the call at `at` of a transcript's calls in the order they ran: null for a call that did not fail. The calls that followed are the later ones of the same prompt and the same run (a
|
|
19
|
+
* subagent's call is not what the main run did next), up to 5. The scan ends at the first later call of that run that belongs to another prompt.
|
|
20
|
+
*/
|
|
21
|
+
export declare const recoveryCallsOf: (calls: readonly Followed[], at: number) => RecoveryCalls | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** How many following calls a failed call lists. */
|
|
2
|
+
export const RECOVERY_CALLS = 5;
|
|
3
|
+
/** The actions that write a file: the stored error row's `editFollowed` counts the same tools (`Edit`, `Write`, `MultiEdit`). */
|
|
4
|
+
const WRITES = new Set(['file.edit', 'file.write']);
|
|
5
|
+
/** A call's action is its first step's; a call with no step is `other`. */
|
|
6
|
+
const actionOf = ({ steps }) => steps?.[0]?.action ?? 'other';
|
|
7
|
+
/**
|
|
8
|
+
* What followed the call at `at` of a transcript's calls in the order they ran: null for a call that did not fail. The calls that followed are the later ones of the same prompt and the same run (a
|
|
9
|
+
* subagent's call is not what the main run did next), up to 5. The scan ends at the first later call of that run that belongs to another prompt.
|
|
10
|
+
*/
|
|
11
|
+
export const recoveryCallsOf = (calls, at) => {
|
|
12
|
+
const failed = calls[at];
|
|
13
|
+
if (failed === undefined || failed.status !== 'error')
|
|
14
|
+
return null;
|
|
15
|
+
const next = [];
|
|
16
|
+
for (let index = at + 1; index < calls.length && next.length < RECOVERY_CALLS; index += 1) {
|
|
17
|
+
const call = calls[index];
|
|
18
|
+
if (call === undefined || call.side !== failed.side)
|
|
19
|
+
continue;
|
|
20
|
+
if (call.prompt !== failed.prompt)
|
|
21
|
+
break;
|
|
22
|
+
next.push(actionOf(call));
|
|
23
|
+
}
|
|
24
|
+
return { next, editFollowed: next.some((action) => WRITES.has(action)) };
|
|
25
|
+
};
|
|
@@ -4,3 +4,14 @@ export declare const entropy: (text: string) => number;
|
|
|
4
4
|
export type Redactor = (text: string) => string;
|
|
5
5
|
/** `domains` are the organization domains to mask (`example.com` also masks `mail.example.com`). */
|
|
6
6
|
export declare const makeRedactor: (domains?: readonly string[]) => Redactor;
|
|
7
|
+
/** How many values of a redacted text were masked: each placeholder is one. */
|
|
8
|
+
export declare const placeholders: (text: string) => number;
|
|
9
|
+
/** The names this machine's person goes by in a transcript: the account's and the home folder's. A name under three characters would mask ordinary words, so it is left to the path rules. */
|
|
10
|
+
export declare const localNames: (home?: string, account?: () => string) => string[];
|
|
11
|
+
/** A whole transcript body redacted for upload, and how many values were masked (placeholders added, a count and nothing else). `names` are the local usernames to mask wherever they stand as a whole word. */
|
|
12
|
+
export declare const redactTranscript: (body: string, domains?: readonly string[], names?: readonly string[]) => {
|
|
13
|
+
text: string;
|
|
14
|
+
masked: number;
|
|
15
|
+
};
|
|
16
|
+
/** The version of the rules above, stated to the pool with every raw part; a change to any rule of `redactTranscript` raises it. */
|
|
17
|
+
export declare const REDACTION_VERSION = "1";
|
package/dist/sessions/redact.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { homedir, userInfo } from 'node:os';
|
|
2
|
+
import { basename } from 'node:path';
|
|
1
3
|
// Redaction before anything is written to the session store. Deterministic and conservative: it would rather over-mask a
|
|
2
4
|
// long random-looking token than let a secret through. Pattern rules first, then emails, the organization domains the
|
|
3
5
|
// operator names, and last an entropy rule for long mixed-case tokens no pattern knew.
|
|
@@ -52,3 +54,60 @@ export const makeRedactor = (domains = []) => {
|
|
|
52
54
|
return out.replace(TOKEN, (token) => (looksRandom(token) ? '<secret>' : token));
|
|
53
55
|
};
|
|
54
56
|
};
|
|
57
|
+
// What a whole transcript holds beyond what a slice of one does (t884): home paths and usernames, environment values, and hosts. A raw copy that
|
|
58
|
+
// leaves the machine goes through these after `makeRedactor`'s rules. Best effort, not a proof: `docs/sessions-store.md` ("The raw copy") lists what it cannot catch.
|
|
59
|
+
const URL_HOST = /(\b[a-z][a-z0-9+.-]*:\/\/)(?:[^\s/@"'\\]*@)?[A-Za-z0-9.-]+(?::\d+)?/gi;
|
|
60
|
+
const IPV4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
61
|
+
const INTERNAL_NAME = /\b(?:[a-z0-9-]+\.)+(?:internal|local|lan|corp|intranet|home|localdomain)\b/gi;
|
|
62
|
+
const UNIX_HOME = /(\/(?:home|Users)\/)(?![<.])[^/\s"'\\]+/g;
|
|
63
|
+
const WINDOWS_HOME = /([A-Za-z]:[\\/]+Users[\\/]+)(?![<.])[^\\/\s"']+/gi;
|
|
64
|
+
const ENVIRONMENT = /\b((?:export\s+)?[A-Z][A-Z0-9_]{2,}=)(?!<)(?:\\?["'][^"'\\]*\\?["']|[^\s"'\\]+)/g;
|
|
65
|
+
// The harness names a project folder after its path: `/Users/name/work` becomes `-Users-name-work`, and `C:\\Users\\name\\work` becomes `C--Users-name-work`. The name runs to the next hyphen, so a hyphenated name is masked whole earlier, by the local-name rule (which also matches each name in this encoded form).
|
|
66
|
+
const ENCODED_HOME = /(?<![A-Za-z0-9])((?:[A-Za-z]-)?-(?:Users|home)-)(?!<)[^-\s"'\\/]+/g;
|
|
67
|
+
// A shell prompt, `name@box:~/dir$`: a box name with no dot is not an address, so the email rule leaves it.
|
|
68
|
+
const PROMPT = /(?:<user>|\b[A-Za-z0-9._-]+)@[A-Za-z0-9-]+(?=:[~/.])/g;
|
|
69
|
+
// Full, compressed, and loopback forms. A clock time (`10:20:30`) has no `::`, so it is never one.
|
|
70
|
+
const IPV6 = /(?<![0-9A-Za-z:])(?:(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,7}:(?:[0-9a-f]{1,4}(?::[0-9a-f]{1,4}){0,6})?|::[0-9a-f]{1,4}(?::[0-9a-f]{1,4}){0,6})(?![0-9A-Za-z:])/gi;
|
|
71
|
+
const MIN_NAME = 3;
|
|
72
|
+
const PLACEHOLDER = /<(?:secret|email|org-domain|host|user|env)>/g;
|
|
73
|
+
/** How many values of a redacted text were masked: each placeholder is one. */
|
|
74
|
+
export const placeholders = (text) => text.match(PLACEHOLDER)?.length ?? 0;
|
|
75
|
+
/** The account name; `userInfo()` throws when the uid has no passwd entry (a container run with `--user <uid>`), so the environment stands in. */
|
|
76
|
+
const accountName = () => userInfo().username;
|
|
77
|
+
/** The names this machine's person goes by in a transcript: the account's and the home folder's. A name under three characters would mask ordinary words, so it is left to the path rules. */
|
|
78
|
+
export const localNames = (home = homedir(), account = accountName) => {
|
|
79
|
+
const read = () => {
|
|
80
|
+
try {
|
|
81
|
+
return account();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return process.env['USER'] || process.env['USERNAME'] || '';
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return [...new Set([read(), basename(home)].filter((name) => name.length >= MIN_NAME))];
|
|
88
|
+
};
|
|
89
|
+
// Each name also stands in its encoded-folder form (`John Smith` is `John-Smith` in `C--Users-John-Smith-repo`), and the longest goes first so `mary-jane` is never cut to `mary`.
|
|
90
|
+
const nameRule = (names) => {
|
|
91
|
+
const usable = names.filter((name) => name.length >= MIN_NAME);
|
|
92
|
+
const forms = [...new Set(usable.flatMap((name) => [name, name.replace(/[^A-Za-z0-9]/g, '-')]))].sort((a, b) => b.length - a.length);
|
|
93
|
+
return forms.length === 0 ? undefined : new RegExp(`(?<![A-Za-z0-9<])(?:${forms.map(escapeRegExp).join('|')})(?![A-Za-z0-9>])`, 'gi');
|
|
94
|
+
};
|
|
95
|
+
/** A whole transcript body redacted for upload, and how many values were masked (placeholders added, a count and nothing else). `names` are the local usernames to mask wherever they stand as a whole word. */
|
|
96
|
+
export const redactTranscript = (body, domains = [], names = []) => {
|
|
97
|
+
const named = nameRule(names);
|
|
98
|
+
// The names go first: a hyphenated or spaced name must be masked whole before the path rules take its first word.
|
|
99
|
+
const known = makeRedactor(domains)(body);
|
|
100
|
+
const redacted = (named === undefined ? known : known.replace(named, '<user>'))
|
|
101
|
+
.replace(URL_HOST, '$1<host>')
|
|
102
|
+
.replace(INTERNAL_NAME, '<host>')
|
|
103
|
+
.replace(IPV4, '<host>')
|
|
104
|
+
.replace(IPV6, '<host>')
|
|
105
|
+
.replace(UNIX_HOME, '$1<user>')
|
|
106
|
+
.replace(WINDOWS_HOME, '$1<user>')
|
|
107
|
+
.replace(ENCODED_HOME, '$1<user>')
|
|
108
|
+
.replace(PROMPT, '<user>@<host>')
|
|
109
|
+
.replace(ENVIRONMENT, '$1<env>');
|
|
110
|
+
return { text: redacted, masked: Math.max(0, placeholders(redacted) - placeholders(body)) };
|
|
111
|
+
};
|
|
112
|
+
/** The version of the rules above, stated to the pool with every raw part; a change to any rule of `redactTranscript` raises it. */
|
|
113
|
+
export const REDACTION_VERSION = '1';
|