@manyoars/oars 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -29
- package/dist/bin.js +3 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +206 -20
- package/dist/club.d.ts +19 -0
- package/dist/club.js +132 -0
- package/dist/gate/hook-fill-main.js +1 -1
- package/dist/gate/hook-fill.d.ts +1 -1
- package/dist/gate/hook-fill.js +1 -1
- package/dist/gate/hook-main.js +7 -5
- package/dist/gate/hook.d.ts +12 -1
- package/dist/gate/hook.js +38 -7
- package/dist/hook-bin.js +23 -4
- package/dist/main.js +5 -0
- package/dist/node-floor.d.ts +10 -0
- package/dist/node-floor.js +23 -0
- package/dist/oars-hook.d.ts +2 -0
- package/dist/oars-hook.js +7 -0
- package/dist/sessions/allow.d.ts +1 -0
- package/dist/sessions/allow.js +1 -1
- package/dist/sessions/call-shape.d.ts +111 -0
- package/dist/sessions/call-shape.js +464 -0
- package/dist/sessions/candidates.d.ts +65 -0
- package/dist/sessions/candidates.js +236 -0
- package/dist/sessions/card-command.d.ts +24 -0
- package/dist/sessions/card-command.js +71 -0
- package/dist/sessions/card-font.d.ts +6 -0
- package/dist/sessions/card-font.js +36 -0
- package/dist/sessions/card-line.d.ts +30 -0
- package/dist/sessions/card-line.js +105 -0
- package/dist/sessions/card-render.d.ts +15 -0
- package/dist/sessions/card-render.js +146 -0
- package/dist/sessions/card.d.ts +59 -0
- package/dist/sessions/card.js +107 -0
- package/dist/sessions/cell-increments.d.ts +14 -3
- package/dist/sessions/cell-increments.js +79 -12
- package/dist/sessions/cells.d.ts +42 -4
- package/dist/sessions/cells.js +72 -18
- package/dist/sessions/codex-hook-bin.d.ts +1 -0
- package/dist/sessions/codex-hook-bin.js +24 -0
- package/dist/sessions/codex-hook-install.d.ts +21 -0
- package/dist/sessions/codex-hook-install.js +47 -0
- package/dist/sessions/codex-hook-view.d.ts +20 -0
- package/dist/sessions/codex-hook-view.js +20 -0
- package/dist/sessions/codex-hook.d.ts +77 -0
- package/dist/sessions/codex-hook.js +255 -0
- package/dist/sessions/codex.d.ts +9 -0
- package/dist/sessions/codex.js +104 -18
- package/dist/sessions/command.d.ts +17 -2
- package/dist/sessions/command.js +145 -33
- package/dist/sessions/copilot.d.ts +3 -0
- package/dist/sessions/copilot.js +12 -4
- package/dist/sessions/derive.js +2 -1
- package/dist/sessions/empty-state.d.ts +17 -0
- package/dist/sessions/empty-state.js +38 -0
- package/dist/sessions/exclusion.js +48 -3
- package/dist/sessions/extract.d.ts +16 -3
- package/dist/sessions/extract.js +281 -24
- package/dist/sessions/gemini.d.ts +2 -0
- package/dist/sessions/gemini.js +29 -2
- package/dist/sessions/harness-files.d.ts +27 -0
- package/dist/sessions/harness-files.js +109 -0
- package/dist/sessions/head-only.d.ts +7 -0
- package/dist/sessions/head-only.js +21 -0
- package/dist/sessions/hook-calls.js +4 -3
- package/dist/sessions/hooks.d.ts +1 -1
- package/dist/sessions/hooks.js +6 -2
- package/dist/sessions/identity.d.ts +6 -0
- package/dist/sessions/injected.d.ts +13 -0
- package/dist/sessions/injected.js +212 -0
- package/dist/sessions/install.d.ts +31 -20
- package/dist/sessions/install.js +80 -82
- package/dist/sessions/load-status.d.ts +41 -0
- package/dist/sessions/load-status.js +156 -0
- package/dist/sessions/machine-child-deps.d.ts +5 -0
- package/dist/sessions/machine-child-deps.js +83 -0
- package/dist/sessions/machine-child.d.ts +43 -0
- package/dist/sessions/machine-child.js +120 -0
- package/dist/sessions/machine-credential.d.ts +10 -0
- package/dist/sessions/machine-credential.js +20 -0
- package/dist/sessions/machine-enroll.d.ts +47 -0
- package/dist/sessions/machine-enroll.js +230 -0
- package/dist/sessions/machine-hold.d.ts +49 -0
- package/dist/sessions/machine-hold.js +88 -0
- package/dist/sessions/machine-ids.d.ts +6 -0
- package/dist/sessions/machine-ids.js +9 -0
- package/dist/sessions/machine-notice.d.ts +21 -0
- package/dist/sessions/machine-notice.js +89 -0
- package/dist/sessions/machine-pull.d.ts +40 -0
- package/dist/sessions/machine-pull.js +220 -0
- package/dist/sessions/machine-repo.d.ts +7 -0
- package/dist/sessions/machine-repo.js +30 -0
- package/dist/sessions/machine-scan-cache.d.ts +12 -0
- package/dist/sessions/machine-scan-cache.js +47 -0
- package/dist/sessions/machine-send.d.ts +12 -0
- package/dist/sessions/machine-send.js +156 -0
- package/dist/sessions/machine-stop.d.ts +22 -0
- package/dist/sessions/machine-stop.js +53 -0
- package/dist/sessions/machine.d.ts +41 -0
- package/dist/sessions/machine.js +89 -0
- package/dist/sessions/opencode.d.ts +2 -0
- package/dist/sessions/opencode.js +6 -0
- package/dist/sessions/otel.d.ts +21 -0
- package/dist/sessions/otel.js +109 -38
- package/dist/sessions/owner-only.d.ts +7 -0
- package/dist/sessions/owner-only.js +34 -0
- package/dist/sessions/pi.d.ts +13 -1
- package/dist/sessions/pi.js +27 -13
- package/dist/sessions/pool-books.d.ts +1 -1
- package/dist/sessions/pool-books.js +1 -1
- package/dist/sessions/pool-choices.d.ts +4 -2
- package/dist/sessions/pool-choices.js +4 -2
- package/dist/sessions/pool-command.d.ts +46 -6
- package/dist/sessions/pool-command.js +577 -133
- package/dist/sessions/pool-delta.d.ts +7 -0
- package/dist/sessions/pool-delta.js +28 -5
- package/dist/sessions/pool-due.d.ts +43 -0
- package/dist/sessions/pool-due.js +113 -0
- package/dist/sessions/pool-nightly.d.ts +9 -1
- package/dist/sessions/pool-nightly.js +65 -21
- package/dist/sessions/pool-outbox.d.ts +51 -5
- package/dist/sessions/pool-outbox.js +217 -24
- package/dist/sessions/pool-print.d.ts +1 -1
- package/dist/sessions/pool-raw.js +4 -3
- package/dist/sessions/pool-replica.d.ts +4 -4
- package/dist/sessions/pool-replica.js +26 -7
- package/dist/sessions/pool-send-lock.js +1 -1
- package/dist/sessions/pool-sent.d.ts +2 -0
- package/dist/sessions/pool-sent.js +21 -4
- package/dist/sessions/pool-wire.d.ts +172 -12
- package/dist/sessions/pool-wire.js +284 -30
- package/dist/sessions/pool.js +4 -2
- package/dist/sessions/public-vocabulary.d.ts +5 -0
- package/dist/sessions/public-vocabulary.js +16 -0
- package/dist/sessions/read-call-shape.js +12 -6
- package/dist/sessions/read-fit.d.ts +5 -0
- package/dist/sessions/read-fit.js +23 -2
- package/dist/sessions/read.d.ts +1 -1
- package/dist/sessions/recall-consequence.d.ts +2 -2
- package/dist/sessions/recall-consequence.js +14 -9
- package/dist/sessions/recall-object.d.ts +9 -2
- package/dist/sessions/receipt.d.ts +8 -0
- package/dist/sessions/receipt.js +78 -0
- package/dist/sessions/recovery-calls.d.ts +3 -2
- package/dist/sessions/recovery-calls.js +9 -3
- package/dist/sessions/redact.d.ts +2 -2
- package/dist/sessions/redact.js +116 -14
- package/dist/sessions/reversion.d.ts +8 -0
- package/dist/sessions/reversion.js +114 -0
- package/dist/sessions/schema.d.ts +78 -4
- package/dist/sessions/schema.js +31 -1
- package/dist/sessions/session-shape.d.ts +2 -1
- package/dist/sessions/session-shape.js +15 -0
- package/dist/sessions/sources.d.ts +9 -0
- package/dist/sessions/sources.js +16 -5
- package/dist/sessions/steps.js +3 -2
- package/dist/sessions/store.d.ts +41 -2
- package/dist/sessions/store.js +68 -16
- package/dist/sessions/try-demo.d.ts +8 -0
- package/dist/sessions/try-demo.js +90 -0
- package/dist/sessions/try.d.ts +13 -0
- package/dist/sessions/try.js +50 -0
- package/dist/sessions/vscode-copilot.d.ts +4 -0
- package/dist/sessions/vscode-copilot.js +25 -18
- package/dist/telemetry/command.js +12 -6
- package/dist/telemetry/consent.d.ts +9 -3
- package/dist/telemetry/consent.js +25 -9
- package/dist/telemetry/fields.d.ts +11 -0
- package/dist/telemetry/fields.js +56 -0
- package/dist/telemetry/measures.d.ts +13 -0
- package/dist/telemetry/measures.js +30 -0
- package/dist/telemetry/observe.d.ts +15 -2
- package/dist/telemetry/observe.js +34 -7
- package/dist/telemetry/region.d.ts +6 -0
- package/dist/telemetry/region.js +28 -0
- package/dist/telemetry/report.js +1 -1
- package/dist/telemetry/schema.d.ts +3 -0
- package/dist/telemetry/schema.js +11 -1
- package/dist/telemetry/send.d.ts +5 -2
- package/dist/telemetry/send.js +15 -7
- package/dist/telemetry/words.d.ts +6 -0
- package/dist/telemetry/words.js +6 -0
- package/package.json +23 -4
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
// Call shape (the fit family's other method): one tool and its input key names, over the floors a shape needs before a hook may push it. A shape is what a pre-tool hook can
|
|
2
|
-
// compute from the call it is about to run, so this read is what a push cites.
|
|
3
|
-
//
|
|
4
|
-
|
|
2
|
+
// compute from the call it is about to run, so this read is what a push cites. Its evidence is the shape cells that count the action's calls by those key names (`shape:<action>/keys/<names>`, a week
|
|
3
|
+
// cell per version bucket, `shapeClass`), in the weeks the shape failed. Same confounders as `read-fit.ts`: a failure is not a misfit, and prompt class is not adjusted for. A bundle holds no store, so the
|
|
4
|
+
// pool serves the same cells as `fit-shape` (`deriveFitShape`), by every facet of shape and not this one tool's.
|
|
5
|
+
import { lastDayOfCell, parseShapeClass, UNKNOWN } from './cells.js';
|
|
6
|
+
import { OPEN_GATE, wireKeys } from './pool-wire.js';
|
|
5
7
|
import { CALL_SHAPE_METHOD } from './read.js';
|
|
6
8
|
import { actionOfShape, fitReads, toolShapeId } from './recall-reads.js';
|
|
7
9
|
import { body, atGrain, rate, wilson } from './read-support.js';
|
|
8
10
|
export const deriveCallShape = ({ pool, store }) => fitReads(store).flatMap((read) => {
|
|
9
11
|
const action = actionOfShape(read.tool, read.harnesses);
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
12
|
+
// The cell names the keys as the wire sends them (`projectedStore`); a record captured before steps was never projected, so its raw names are the other spelling.
|
|
13
|
+
const names = new Set([wireKeys(read.keys, read.tool, OPEN_GATE).keys.join(','), read.keys.join(',')]);
|
|
14
|
+
const within = (cell) => read.firstDay !== null && read.lastDay !== null && cell.day <= read.lastDay && lastDayOfCell(cell) >= read.firstDay;
|
|
15
|
+
const cells = atGrain(pool, 'week').filter((cell) => {
|
|
16
|
+
const shape = cell.day === UNKNOWN ? null : parseShapeClass(pool.motifs.get(cell.motifId)?.cls ?? '');
|
|
17
|
+
return shape !== null && shape.action === action && shape.facet === 'keys' && names.has(shape.value) && cell.failed > 0 && within(cell);
|
|
18
|
+
});
|
|
13
19
|
// A shape whose failures are in no cell has nothing to cite, so it is not a read: its answers keep their stand-in.
|
|
14
20
|
if (cells.length === 0)
|
|
15
21
|
return [];
|
|
@@ -5,3 +5,8 @@ export declare const deriveFit: DeriveFamily;
|
|
|
5
5
|
* this read is its own, and a read of the version cells does not see these cells (`atGrain`). A shell the frame does not name reads as `unknown`, as a missing model does.
|
|
6
6
|
*/
|
|
7
7
|
export declare const deriveFitEnvironment: DeriveFamily;
|
|
8
|
+
/**
|
|
9
|
+
* Fit by call shape: how often calls of an action fail when they hold one facet of shape (a target class, an argument class, a count of unsent key names, a set of key names, or a value feature, closed value, or change kind of a sent key), by harness version bucket, from the
|
|
10
|
+
* shape cells (`shapeClass`). A cell counts each call once per facet it holds, so the facets of one action are never summed into one rate. Same confounders as the reads above, and a facet is not the failure's cause.
|
|
11
|
+
*/
|
|
12
|
+
export declare const deriveFitShape: DeriveFamily;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Fit (rank 2): how often a tool fails on this frame, by harness version bucket and model. Effort is not in a cell (rule 3), so it is not a slice. Confounders it does not remove: a failure is not a
|
|
2
2
|
// misfit, red tests can be the method, effort, and prompt class is not adjusted for.
|
|
3
|
-
import { bucketFields, environmentFieldsOf } from './cells.js';
|
|
3
|
+
import { bucketFields, environmentFieldsOf, parseShapeClass } from './cells.js';
|
|
4
4
|
import { motifName } from './motif.js';
|
|
5
|
-
import { body, environmentCells, groupBy, rate, settled, total, unitCells, wilson } from './read-support.js';
|
|
5
|
+
import { atGrain, body, environmentCells, groupBy, rate, settled, total, unitCells, wilson } from './read-support.js';
|
|
6
6
|
export const deriveFit = ({ pool }) => [...groupBy(unitCells(pool, 'week'), ({ cell }) => `${cell.motifId}/${cell.frameBucket}`).values()].flatMap((group) => {
|
|
7
7
|
const [first] = group;
|
|
8
8
|
const cells = group.map(({ cell }) => cell);
|
|
@@ -39,3 +39,24 @@ export const deriveFitEnvironment = ({ pool }) => [...groupBy(environmentCells(p
|
|
|
39
39
|
}),
|
|
40
40
|
];
|
|
41
41
|
});
|
|
42
|
+
/**
|
|
43
|
+
* Fit by call shape: how often calls of an action fail when they hold one facet of shape (a target class, an argument class, a count of unsent key names, a set of key names, or a value feature, closed value, or change kind of a sent key), by harness version bucket, from the
|
|
44
|
+
* shape cells (`shapeClass`). A cell counts each call once per facet it holds, so the facets of one action are never summed into one rate. Same confounders as the reads above, and a facet is not the failure's cause.
|
|
45
|
+
*/
|
|
46
|
+
export const deriveFitShape = ({ pool }) => [...groupBy(atGrain(pool, 'week'), (cell) => `${cell.motifId}/${cell.frameBucket}`).values()].flatMap((group) => {
|
|
47
|
+
const [first] = group;
|
|
48
|
+
const shape = first === undefined ? null : parseShapeClass(pool.motifs.get(first.motifId)?.cls ?? '');
|
|
49
|
+
const n = total(group, settled);
|
|
50
|
+
if (first === undefined || shape === null || n === 0)
|
|
51
|
+
return [];
|
|
52
|
+
const failed = total(group, (cell) => cell.failed);
|
|
53
|
+
const { harness, harnessBucket } = bucketFields(first.frameBucket);
|
|
54
|
+
return [
|
|
55
|
+
body('fit', group, {
|
|
56
|
+
slice: { action: shape.action, facet: shape.facet, value: shape.value, harness, harnessBucket },
|
|
57
|
+
value: { failureRate: rate(failed, n), failed, ok: n - failed },
|
|
58
|
+
n,
|
|
59
|
+
interval: wilson(failed, n),
|
|
60
|
+
}),
|
|
61
|
+
];
|
|
62
|
+
});
|
package/dist/sessions/read.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface Read {
|
|
|
69
69
|
interval: ReadInterval | null;
|
|
70
70
|
asOf: ReadAsOf;
|
|
71
71
|
basis: ReadBasis;
|
|
72
|
-
/** The floor
|
|
72
|
+
/** The lowest floor among the cells it cites, each cell's being the floor of its class in the read's family (`cellFloor`): every cell it counts had at least this many clusters. Stated by a pool that serves the read, and absent from a read computed on one operator's own store, which applies no public floor. */
|
|
73
73
|
floor?: number;
|
|
74
74
|
}
|
|
75
75
|
/** A read before its inputs are checked and its ID and order are computed. */
|
|
@@ -31,9 +31,9 @@ export interface RecallCountRow {
|
|
|
31
31
|
sessions: number;
|
|
32
32
|
/** The size of what was injected, or for a held recall would have been. */
|
|
33
33
|
tokens: number;
|
|
34
|
-
/** Recalls with a consequence window, and of those how many
|
|
34
|
+
/** Recalls with a consequence window, and of those how many have a call whose action name is one the recall carried. */
|
|
35
35
|
windows: number;
|
|
36
|
-
|
|
36
|
+
nameMatched: number;
|
|
37
37
|
windowCalls: number;
|
|
38
38
|
/** Null when no window in the row measured any tokens. */
|
|
39
39
|
windowTokens: number | null;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// shown recall touches the graph, because a held one reached nobody: it adds no `recall.*` step and exposes no cell.
|
|
4
4
|
import { motifsOf, stepsOfCall } from './motif.js';
|
|
5
5
|
import { bucketOf, floorOf } from './pool-wire.js';
|
|
6
|
+
import { reversionsOf } from './reversion.js';
|
|
6
7
|
import { harnessOf } from './schema.js';
|
|
7
8
|
/** A consequence window ends at the prompt's end or after this many calls (spec Section 2.7). */
|
|
8
9
|
export const WINDOW_CALLS = 20;
|
|
@@ -35,14 +36,16 @@ const positionOf = (line, at) => {
|
|
|
35
36
|
const next = line.findIndex((tool) => tool.ts !== null && tool.ts > at);
|
|
36
37
|
return next < 0 ? line.length : next;
|
|
37
38
|
};
|
|
38
|
-
|
|
39
|
+
/** The values the prompt first showed, and beside them, never in place of them, the nearest later revert of a commit the prompt made (null when none is tied). */
|
|
40
|
+
const outcomeOf = (prompt, reverted) => ({
|
|
39
41
|
errors: prompt?.errors ?? null,
|
|
40
42
|
verify: prompt?.verify ?? null,
|
|
41
43
|
stopped: prompt?.stopped ?? null,
|
|
42
44
|
commits: prompt?.commits ?? null,
|
|
43
45
|
redo: prompt?.redo ?? null,
|
|
46
|
+
reverted: prompt === undefined ? null : reverted,
|
|
44
47
|
});
|
|
45
|
-
const consequenceOf = (recall, session, line, position, harness, prompts) => {
|
|
48
|
+
const consequenceOf = (recall, session, line, position, harness, prompts, reverted) => {
|
|
46
49
|
const prompt = (line[position - 1] ?? line[position])?.prompt ?? null;
|
|
47
50
|
const calls = [];
|
|
48
51
|
let end = 'session';
|
|
@@ -64,9 +67,10 @@ const consequenceOf = (recall, session, line, position, harness, prompts) => {
|
|
|
64
67
|
window: { from: position, to: position + calls.length, calls: calls.length, end },
|
|
65
68
|
// Shape level only: it is the ID that joins across accounts, and named windows would multiply a row that exists per recall.
|
|
66
69
|
motifs: [...new Set(motifsOf(steps).flatMap(({ motif }) => (motif.level === 'shape' ? [motif.motifId] : [])))],
|
|
67
|
-
|
|
70
|
+
// The action name is the whole basis: a call of the shown action that failed, or did something else with it, still matches. The record holds no stronger evidence yet, so none is implied.
|
|
71
|
+
nameMatched: steps.some((step) => names.has(step.action)),
|
|
68
72
|
cost: { tokens: measured.length === 0 ? null : measured.reduce((sum, tokens) => sum + tokens, 0), durationMs: steps.reduce((sum, step) => sum + step.durationMs, 0) },
|
|
69
|
-
outcome: outcomeOf(prompts.get(promptKey(session, prompt))),
|
|
73
|
+
outcome: outcomeOf(prompts.get(promptKey(session, prompt)), reverted(session, prompt)),
|
|
70
74
|
};
|
|
71
75
|
};
|
|
72
76
|
/** Fills `n` and `consequence`. A recall with no session, or from a session this store does not hold, has nothing to read and comes back as it was. */
|
|
@@ -74,13 +78,14 @@ export const deriveConsequences = (store, recalls) => {
|
|
|
74
78
|
const lines = mainLines(store);
|
|
75
79
|
const prompts = new Map(store.prompts.map((prompt) => [promptKey(prompt.session, prompt.index), prompt]));
|
|
76
80
|
const sessions = new Map(store.sessions.map((session) => [session.session, session]));
|
|
81
|
+
const reverted = reversionsOf(store);
|
|
77
82
|
return recalls.map((recall) => {
|
|
78
83
|
const session = recall.sid === null ? undefined : sessions.get(recall.sid);
|
|
79
84
|
if (session === undefined)
|
|
80
85
|
return recall;
|
|
81
86
|
const line = lines.get(session.session) ?? [];
|
|
82
87
|
const position = positionOf(line, recall.at);
|
|
83
|
-
return { ...recall, n: position, consequence: consequenceOf(recall, session.session, line, position, harnessOf(session), prompts) };
|
|
88
|
+
return { ...recall, n: position, consequence: consequenceOf(recall, session.session, line, position, harnessOf(session), prompts, reverted) };
|
|
84
89
|
});
|
|
85
90
|
};
|
|
86
91
|
export const NO_RECALLS = {
|
|
@@ -150,7 +155,7 @@ export const recallCounts = (recalls, day) => {
|
|
|
150
155
|
if (day !== undefined && !recall.at.startsWith(day))
|
|
151
156
|
continue;
|
|
152
157
|
const key = [recall.mode, recall.arm, recall.answer.kind].join(' ');
|
|
153
|
-
const held = rows.get(key) ?? { row: { mode: recall.mode, arm: recall.arm, kind: recall.answer.kind, recalls: 0, sessions: 0, tokens: 0, windows: 0,
|
|
158
|
+
const held = rows.get(key) ?? { row: { mode: recall.mode, arm: recall.arm, kind: recall.answer.kind, recalls: 0, sessions: 0, tokens: 0, windows: 0, nameMatched: 0, windowCalls: 0, windowTokens: null, overlapped: 0 }, sessions: new Set() };
|
|
154
159
|
rows.set(key, held);
|
|
155
160
|
const { row, sessions } = held;
|
|
156
161
|
row.recalls += 1;
|
|
@@ -162,8 +167,8 @@ export const recallCounts = (recalls, day) => {
|
|
|
162
167
|
if (consequence === undefined)
|
|
163
168
|
continue;
|
|
164
169
|
row.windows += 1;
|
|
165
|
-
if (consequence.
|
|
166
|
-
row.
|
|
170
|
+
if (consequence.nameMatched)
|
|
171
|
+
row.nameMatched += 1;
|
|
167
172
|
row.windowCalls += consequence.window.calls;
|
|
168
173
|
if (consequence.cost.tokens !== null)
|
|
169
174
|
row.windowTokens = (row.windowTokens ?? 0) + consequence.cost.tokens;
|
|
@@ -175,5 +180,5 @@ export const recallCounts = (recalls, day) => {
|
|
|
175
180
|
export const formatRecallCounts = (rows) => rows.length === 0
|
|
176
181
|
? 'No recalls in that span.'
|
|
177
182
|
: rows
|
|
178
|
-
.map((row) => `${row.mode} ${row.arm} ${row.kind} | ${String(row.recalls)} recalls, ${String(row.sessions)} sessions, ${String(row.tokens)} tokens | ${String(row.windows)} windows, ${String(row.
|
|
183
|
+
.map((row) => `${row.mode} ${row.arm} ${row.kind} | ${String(row.recalls)} recalls, ${String(row.sessions)} sessions, ${String(row.tokens)} tokens | ${String(row.windows)} windows, ${String(row.nameMatched)} name-matched, ${String(row.windowCalls)} calls, ${row.windowTokens === null ? 'window tokens unmeasured' : `${String(row.windowTokens)} window tokens`}, ${String(row.overlapped)} overlapped`)
|
|
179
184
|
.join('\n');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Reversion } from './reversion.js';
|
|
1
2
|
import type { Frame } from './schema.js';
|
|
2
3
|
export declare const recallsPath: (storeDir: string) => string;
|
|
3
4
|
/** The share of pushes held back (spec Section 8, open parameter). */
|
|
@@ -38,7 +39,7 @@ export interface RecallAnswer {
|
|
|
38
39
|
bytes?: number;
|
|
39
40
|
/** What the asker saw, or for a held recall would have seen. It holds structured fields only (`recall-render.ts`). */
|
|
40
41
|
rendered: string;
|
|
41
|
-
/** The closed names a
|
|
42
|
+
/** The closed names a name match can find: canonical actions, in the order shown. */
|
|
42
43
|
names: string[];
|
|
43
44
|
}
|
|
44
45
|
export type DeltaAxis = 'same' | 'differs' | 'unknown';
|
|
@@ -79,6 +80,11 @@ export interface RecallOutcome {
|
|
|
79
80
|
stopped: string | null;
|
|
80
81
|
commits: number | null;
|
|
81
82
|
redo: boolean | null;
|
|
83
|
+
/**
|
|
84
|
+
* Where the nearest later clean `git revert <sha>` of a commit this prompt made ran, beside the values above and never in place of them. The sha is the whole tie, so null says none was found, not that
|
|
85
|
+
* none happened: an edit undone any other way, or a commit with no sha, is not read.
|
|
86
|
+
*/
|
|
87
|
+
reverted: Reversion | null;
|
|
82
88
|
}
|
|
83
89
|
/** Derived from the capture records, never captured live. */
|
|
84
90
|
export interface RecallConsequence {
|
|
@@ -90,7 +96,8 @@ export interface RecallConsequence {
|
|
|
90
96
|
end: 'prompt' | 'calls' | 'session';
|
|
91
97
|
};
|
|
92
98
|
motifs: string[];
|
|
93
|
-
|
|
99
|
+
/** Whether an action name the recall carried appears among the window's actions. A match on the name alone: it does not say the call took the advice, was the same use of that action, or went well. */
|
|
100
|
+
nameMatched: boolean;
|
|
94
101
|
/** `tokens` is null when no call in the window measured any: an older capture has no step costs, and zero would read as a free window. */
|
|
95
102
|
cost: {
|
|
96
103
|
tokens: number | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RecallQuery } from './query.js';
|
|
2
|
+
import type { Store } from './store.js';
|
|
3
|
+
export declare const RECEIPT_FOOTER = "from my local oars record, manyoars.com";
|
|
4
|
+
export declare const RECEIPT_MISS = "No earlier session in the local record hit this error, so there is no receipt.";
|
|
5
|
+
/** ISO 8601 week of a timestamp, as `2026-W45`; the week holds the Thursday, which fixes the year. */
|
|
6
|
+
export declare const isoWeek: (ts: string) => string | undefined;
|
|
7
|
+
/** The receipt for the best-matching signature of an error text, or null when no earlier session hit it. */
|
|
8
|
+
export declare const receiptOf: (store: Store, query: RecallQuery, now: number, redactDomains?: readonly string[], names?: readonly string[]) => string | null;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// `oars sessions recall --error <text> --receipt`: a fenced Markdown block a person pastes into an issue on the failing tool. It states the tool and
|
|
2
|
+
// harness by listed name, the error's normalized signature, how many earlier sessions hit it and in which weeks, and what those sessions did next. It
|
|
3
|
+
// reads the same match recall does and the same redactor, so nothing here is a second path to a stored string.
|
|
4
|
+
import { ACTION_TABLES } from './actions.js';
|
|
5
|
+
import { redactTranscript } from './redact.js';
|
|
6
|
+
import { recall } from './query.js';
|
|
7
|
+
import { LITERAL } from './public-vocabulary.js';
|
|
8
|
+
import { closedName } from './recall-render.js';
|
|
9
|
+
import { harnessOf } from './schema.js';
|
|
10
|
+
export const RECEIPT_FOOTER = 'from my local oars record, manyoars.com';
|
|
11
|
+
export const RECEIPT_MISS = 'No earlier session in the local record hit this error, so there is no receipt.';
|
|
12
|
+
const OTHER = 'other';
|
|
13
|
+
const SIGNATURE_LIMIT = 120;
|
|
14
|
+
const NEXT_SHOWN = 3;
|
|
15
|
+
const MCP_WORD = /mcp__[A-Za-z0-9_-]+/g;
|
|
16
|
+
const DAY_MS = 86_400_000;
|
|
17
|
+
/** A tool as a person may name it in an issue: a listed name as it stands, anything else `other`. */
|
|
18
|
+
const listedTool = (name) => {
|
|
19
|
+
const listed = closedName(name);
|
|
20
|
+
return listed === LITERAL ? OTHER : listed;
|
|
21
|
+
};
|
|
22
|
+
const listedHarness = (name) => (Object.hasOwn(ACTION_TABLES, name) ? name : OTHER);
|
|
23
|
+
/** ISO 8601 week of a timestamp, as `2026-W45`; the week holds the Thursday, which fixes the year. */
|
|
24
|
+
export const isoWeek = (ts) => {
|
|
25
|
+
const at = Date.parse(ts);
|
|
26
|
+
if (Number.isNaN(at))
|
|
27
|
+
return undefined;
|
|
28
|
+
const day = new Date(Math.floor(at / DAY_MS) * DAY_MS);
|
|
29
|
+
const weekday = day.getUTCDay() === 0 ? 7 : day.getUTCDay();
|
|
30
|
+
const thursday = new Date(day.getTime() + (4 - weekday) * DAY_MS);
|
|
31
|
+
const jan1 = Date.UTC(thursday.getUTCFullYear(), 0, 1);
|
|
32
|
+
const week = Math.floor((thursday.getTime() - jan1) / DAY_MS / 7) + 1;
|
|
33
|
+
return `${String(thursday.getUTCFullYear())}-W${String(week).padStart(2, '0')}`;
|
|
34
|
+
};
|
|
35
|
+
const times = (count) => `${String(count)} ${count === 1 ? 'time' : 'times'}`;
|
|
36
|
+
/** The receipt for the best-matching signature of an error text, or null when no earlier session hit it. */
|
|
37
|
+
export const receiptOf = (store, query, now, redactDomains = [], names = []) => {
|
|
38
|
+
const [top] = recall(store, query, now, 1, redactDomains).matches;
|
|
39
|
+
if (top === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
const rows = store.errors.filter((error) => error.sig === top.sig);
|
|
42
|
+
const sessionIds = new Set(rows.map((error) => error.session));
|
|
43
|
+
const harnesses = [...new Set(store.sessions.filter((session) => sessionIds.has(session.session)).map((session) => listedHarness(harnessOf(session))))].sort();
|
|
44
|
+
const weeks = [...new Set(rows.flatMap((error) => { const week = error.ts === null ? undefined : isoWeek(error.ts); return week === undefined ? [] : [week]; }))].sort().reverse();
|
|
45
|
+
// Listed tool names folded together: two unlisted tools are both `other`, once.
|
|
46
|
+
const next = new Map();
|
|
47
|
+
for (const { tool, count } of top.followedBy)
|
|
48
|
+
next.set(listedTool(tool), (next.get(listedTool(tool)) ?? 0) + count);
|
|
49
|
+
const followed = [...next.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, NEXT_SHOWN).map(([tool, count]) => `${tool} x${String(count)}`);
|
|
50
|
+
// The key is `<tool> | <text>`; the tool is stated by its listed name. The text first loses the literals the store records for the matched sessions
|
|
51
|
+
// (branches, project slugs; the transcript rules have no rule for either), then runs through the transcript rules (home paths, encoded folder slugs,
|
|
52
|
+
// local names, secrets), and only the redacted result is cut. Every unlisted `mcp__` word becomes `other`, with backticks out so the fence holds.
|
|
53
|
+
const recorded = store.sessions.filter((entry) => sessionIds.has(entry.session));
|
|
54
|
+
const labels = new Map();
|
|
55
|
+
for (const entry of recorded) {
|
|
56
|
+
for (const branch of entry.branches)
|
|
57
|
+
labels.set(branch, '<branch>');
|
|
58
|
+
labels.set(entry.project, '<project>');
|
|
59
|
+
}
|
|
60
|
+
// A literal goes only where it stands as a whole token, so a branch named `main` leaves `domain` and `./main.js` alone.
|
|
61
|
+
const literals = [...labels.entries()].filter(([literal]) => literal.length > 0).sort((a, b) => b[0].length - a[0].length || a[0].localeCompare(b[0]));
|
|
62
|
+
const scrubbed = literals.reduce((text, [literal, label]) => text.replace(new RegExp(`(?<![A-Za-z0-9_-])${literal.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_-])`, 'g'), label), top.sig.slice(top.sig.indexOf(' | ') + 3));
|
|
63
|
+
const text = redactTranscript(scrubbed, redactDomains, names).text.slice(0, SIGNATURE_LIMIT).replace(MCP_WORD, (word) => listedTool(word)).replaceAll('`', "'");
|
|
64
|
+
return [
|
|
65
|
+
'```markdown',
|
|
66
|
+
'**Error receipt**',
|
|
67
|
+
'',
|
|
68
|
+
`- Tool: ${listedTool(top.tool)}`,
|
|
69
|
+
`- Harness: ${harnesses.join(', ') || OTHER}`,
|
|
70
|
+
`- Error: \`${text}\``,
|
|
71
|
+
`- Earlier sessions that hit it: ${String(top.sessions)} (${times(top.count)})`,
|
|
72
|
+
`- Weeks: ${weeks.join(', ') || 'unknown'}`,
|
|
73
|
+
`- What they did next: ${followed.join(', ') || 'nothing recorded'}; an edit followed ${times(top.editFollowed)}`,
|
|
74
|
+
'',
|
|
75
|
+
RECEIPT_FOOTER,
|
|
76
|
+
'```',
|
|
77
|
+
].join('\n');
|
|
78
|
+
};
|
|
@@ -9,7 +9,7 @@ export type Followed = Pick<ToolObservation, 'side' | 'prompt'> & {
|
|
|
9
9
|
};
|
|
10
10
|
/** How many following calls a failed call lists. */
|
|
11
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. */
|
|
12
|
+
/** What followed one failed call: the actions of the next calls, and whether any of them wrote a file without failing. A write that came back as an error stays in `next` and does not set `editFollowed`. */
|
|
13
13
|
export interface RecoveryCalls {
|
|
14
14
|
next: CanonicalAction[];
|
|
15
15
|
editFollowed: boolean;
|
|
@@ -17,5 +17,6 @@ export interface RecoveryCalls {
|
|
|
17
17
|
/**
|
|
18
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
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
|
+
* `failedEditsCount` is the rule of the wire formats before 6, where any later edit counted whether or not its own result was an error; the ingest checks a batch of those formats with it, so a client already released is not refused for a failed edit.
|
|
20
21
|
*/
|
|
21
|
-
export declare const recoveryCallsOf: (calls: readonly Followed[], at: number) => RecoveryCalls | null;
|
|
22
|
+
export declare const recoveryCallsOf: (calls: readonly Followed[], at: number, failedEditsCount?: boolean) => RecoveryCalls | null;
|
|
@@ -7,19 +7,25 @@ const actionOf = ({ steps }) => steps?.[0]?.action ?? 'other';
|
|
|
7
7
|
/**
|
|
8
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
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
|
+
* `failedEditsCount` is the rule of the wire formats before 6, where any later edit counted whether or not its own result was an error; the ingest checks a batch of those formats with it, so a client already released is not refused for a failed edit.
|
|
10
11
|
*/
|
|
11
|
-
export const recoveryCallsOf = (calls, at) => {
|
|
12
|
+
export const recoveryCallsOf = (calls, at, failedEditsCount = false) => {
|
|
12
13
|
const failed = calls[at];
|
|
13
14
|
if (failed === undefined || failed.status !== 'error')
|
|
14
15
|
return null;
|
|
15
16
|
const next = [];
|
|
17
|
+
let editFollowed = false;
|
|
16
18
|
for (let index = at + 1; index < calls.length && next.length < RECOVERY_CALLS; index += 1) {
|
|
17
19
|
const call = calls[index];
|
|
18
20
|
if (call === undefined || call.side !== failed.side)
|
|
19
21
|
continue;
|
|
20
22
|
if (call.prompt !== failed.prompt)
|
|
21
23
|
break;
|
|
22
|
-
|
|
24
|
+
const action = actionOf(call);
|
|
25
|
+
next.push(action);
|
|
26
|
+
// An edit whose own result is an error wrote nothing, so it is not an edit that followed. A call with no recorded result is not known to have failed, and still counts.
|
|
27
|
+
if (WRITES.has(action) && (failedEditsCount || call.status !== 'error'))
|
|
28
|
+
editFollowed = true;
|
|
23
29
|
}
|
|
24
|
-
return { next, editFollowed
|
|
30
|
+
return { next, editFollowed };
|
|
25
31
|
};
|
|
@@ -9,9 +9,9 @@ export declare const placeholders: (text: string) => number;
|
|
|
9
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
10
|
export declare const localNames: (home?: string, account?: () => string) => string[];
|
|
11
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[]) => {
|
|
12
|
+
export declare const redactTranscript: (body: string, domains?: readonly string[], names?: readonly string[], injectedKey?: string) => {
|
|
13
13
|
text: string;
|
|
14
14
|
masked: number;
|
|
15
15
|
};
|
|
16
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 = "
|
|
17
|
+
export declare const REDACTION_VERSION = "3";
|
package/dist/sessions/redact.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
1
2
|
import { homedir, userInfo } from 'node:os';
|
|
2
3
|
import { basename } from 'node:path';
|
|
4
|
+
import { injectedRule, learnInjected } from './injected.js';
|
|
3
5
|
// Redaction before anything is written to the session store. Deterministic and conservative: it would rather over-mask a
|
|
4
6
|
// long random-looking token than let a secret through. Pattern rules first, then emails, the organization domains the
|
|
5
7
|
// operator names, and last an entropy rule for long mixed-case tokens no pattern knew.
|
|
@@ -70,6 +72,95 @@ const PROMPT = /(?:<user>|\b[A-Za-z0-9._-]+)@[A-Za-z0-9-]+(?=:[~/.])/g;
|
|
|
70
72
|
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
73
|
const MIN_NAME = 3;
|
|
72
74
|
const PLACEHOLDER = /<(?:secret|email|org-domain|host|user|env)>/g;
|
|
75
|
+
// A `.env` line whose name is lower case (`mapbox=q8z7...`): only a whole line of that shape, so `--flag=value` and `?page=2` are left alone.
|
|
76
|
+
const LOWER_ENVIRONMENT = /^([ \t]*(?:export[ \t]+)?[a-z][a-z0-9_]{2,}=)(?!<)(?:(["'])[^"'\n]*\2|[^\s"']+)[ \t]*$/gm;
|
|
77
|
+
// Shapes `makeRedactor` does not know (t955, census S3). They run only on a raw copy, and before the shared rules, because the entropy rule would
|
|
78
|
+
// split a key at its `/` first. A bare 40-character secret key needs a `/` or `+` and a random look, so a git sha (hex) and the front of a
|
|
79
|
+
// base64 image (longer than 40) stay. A password flag is masked only when the value looks like one: four characters with a digit or a symbol, and not a
|
|
80
|
+
// port, so `-p 8080` and `-p3000:3000` stay and `-pthread` is not read as a password.
|
|
81
|
+
const AWS_SECRET_KEY = /(?<![A-Za-z0-9/+=_-])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+=_-])/g;
|
|
82
|
+
const PORT = /^[\d.:]+(?:\/[a-z]+)?$/i;
|
|
83
|
+
const passwordLike = (value) => value.length >= 4 && /[\d!@#$%^&*]/.test(value) && !PORT.test(value);
|
|
84
|
+
const rawRules = (text) => text
|
|
85
|
+
.replace(/-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?(?:-----END PGP PRIVATE KEY BLOCK-----|$)/g, '<secret>')
|
|
86
|
+
.replace(/PuTTY-User-Key-File-\d+:[\s\S]*?Private-Lines:[\s\S]*?(?:Private-MAC:[ \t]*\w*|$)/g, '<secret>')
|
|
87
|
+
.replace(/\bdop_v1_[a-f0-9]{64}\b/g, '<secret>')
|
|
88
|
+
.replace(AWS_SECRET_KEY, (key) => (/[/+]/.test(key) && looksRandom(key) ? '<secret>' : key))
|
|
89
|
+
.replace(/((?<!\S)(?:-u\s*|--user\s+)["']?(?!\d+:)[^\s:"']+:)[^\s"']{4,}/g, '$1<secret>')
|
|
90
|
+
.replace(/(?<!\S)-p(["'])[^"'\n]{4,}\1/g, '-p$1<secret>$1')
|
|
91
|
+
.replace(/(?<!\S)-p([^\s"'-][^\s"']{3,})/g, (flag, value) => (passwordLike(value) ? '-p<secret>' : flag))
|
|
92
|
+
.replace(/(\bsshpass\s+-p\s*)(?:"[^"]*"|'[^']*'|[^\s"']+)/g, '$1<secret>')
|
|
93
|
+
.replace(/((?<!\S)-W\s+)(?:(["'])[^"']+\2|([^\s"'/:-][^\s"'/:]{3,}))/g, (flag, lead, quote, bare) => quote !== undefined || passwordLike(bare ?? '') ? `${lead}<secret>` : flag)
|
|
94
|
+
.replace(/(--(?:password|passwd|pwd)\s+)(?:(["'])[^"']*\2|[^\s"'-][^\s"']*)/g, '$1<secret>');
|
|
95
|
+
// A raw copy is JSONL, so a tool's output sits inside a JSON string where a newline is `\n` and a quote is `\"`: a rule written for plain text
|
|
96
|
+
// misses `\nAKIA...` (no word boundary), cannot cross `\"`, and can eat the `n` of `\n` (t955, census S1, S2, S22). So a line that parses is
|
|
97
|
+
// walked and every string in it redacted decoded (a string that itself holds JSON, in turn); the line is written again only if a value changed,
|
|
98
|
+
// and any other text keeps the plain-text path with its bytes as they were.
|
|
99
|
+
const SECRET_NAME = /password|passwd|secret|token|api[_-]?key|private[_-]?key|credential/i;
|
|
100
|
+
// A count of tokens is a number under a `token` name, so a number is masked only under the names that are always a secret.
|
|
101
|
+
const SECRET_NUMBER_NAME = /password|passwd|secret|api[_-]?key|private[_-]?key|credential/i;
|
|
102
|
+
const CONTAINER = /^\s*[{[]/;
|
|
103
|
+
const isContainer = (value) => typeof value === 'object' && value !== null;
|
|
104
|
+
const parseContainer = (text) => {
|
|
105
|
+
if (!CONTAINER.test(text))
|
|
106
|
+
return undefined;
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(text);
|
|
109
|
+
return isContainer(parsed) ? parsed : undefined;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const redactDecoded = (body, redactText, injected) => {
|
|
116
|
+
// A name reaches the strings of an array under it (`"tokens": ["..."]`); an object's own keys name their values.
|
|
117
|
+
const walk = (value, name) => {
|
|
118
|
+
if (typeof value === 'string')
|
|
119
|
+
return SECRET_NAME.test(name) && value.length >= 4 ? '<secret>' : redactBlock(value);
|
|
120
|
+
if (typeof value === 'number')
|
|
121
|
+
return SECRET_NUMBER_NAME.test(name) && String(value).length >= 4 ? '<secret>' : value;
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
const items = value.map((item) => walk(item, name));
|
|
124
|
+
return items.some((item, at) => item !== value[at]) ? items : value;
|
|
125
|
+
}
|
|
126
|
+
if (typeof value === 'object' && value !== null) {
|
|
127
|
+
const before = Object.entries(value);
|
|
128
|
+
const after = before.map(([key, item]) => [redactBlock(key), walk(item, key)]);
|
|
129
|
+
return after.some(([key, item], at) => key !== before[at]?.[0] || item !== before[at][1]) ? Object.fromEntries(after) : value;
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
};
|
|
133
|
+
// The text as it was when nothing in it changed; otherwise the value written again, inside the whitespace (a `\r`, a newline) it had around it.
|
|
134
|
+
// A transcript line is a top-level object: the harness's own blocks in it are marked first (their hash is of the text as written), then the rest is walked.
|
|
135
|
+
const rewrite = (text, parsed, top) => {
|
|
136
|
+
const marked = top && !Array.isArray(parsed) ? injected(parsed) : parsed;
|
|
137
|
+
const next = walk(marked, '');
|
|
138
|
+
return next === parsed ? text : `${text.slice(0, text.length - text.trimStart().length)}${JSON.stringify(next)}${text.slice(text.trimEnd().length)}`;
|
|
139
|
+
};
|
|
140
|
+
// `text` is a whole document, or lines of which some are JSON: the JSON ones are walked, the runs between them take the plain-text path.
|
|
141
|
+
const redactBlock = (text, top = false) => {
|
|
142
|
+
const whole = parseContainer(text);
|
|
143
|
+
if (whole !== undefined)
|
|
144
|
+
return rewrite(text, whole, top);
|
|
145
|
+
const out = [];
|
|
146
|
+
let run = [];
|
|
147
|
+
for (const line of text.split('\n')) {
|
|
148
|
+
const parsed = parseContainer(line);
|
|
149
|
+
if (parsed === undefined) {
|
|
150
|
+
run.push(line);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (run.length > 0)
|
|
154
|
+
out.push(redactText(run.join('\n')));
|
|
155
|
+
run = [];
|
|
156
|
+
out.push(rewrite(line, parsed, top));
|
|
157
|
+
}
|
|
158
|
+
if (run.length > 0)
|
|
159
|
+
out.push(redactText(run.join('\n')));
|
|
160
|
+
return out.join('\n');
|
|
161
|
+
};
|
|
162
|
+
return redactBlock(body, true);
|
|
163
|
+
};
|
|
73
164
|
/** How many values of a redacted text were masked: each placeholder is one. */
|
|
74
165
|
export const placeholders = (text) => text.match(PLACEHOLDER)?.length ?? 0;
|
|
75
166
|
/** The account name; `userInfo()` throws when the uid has no passwd entry (a container run with `--user <uid>`), so the environment stands in. */
|
|
@@ -93,21 +184,32 @@ const nameRule = (names) => {
|
|
|
93
184
|
return forms.length === 0 ? undefined : new RegExp(`(?<![A-Za-z0-9<])(?:${forms.map(escapeRegExp).join('|')})(?![A-Za-z0-9>])`, 'gi');
|
|
94
185
|
};
|
|
95
186
|
/** 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 = []) => {
|
|
187
|
+
export const redactTranscript = (body, domains = [], names = [], injectedKey = randomBytes(16).toString('hex')) => {
|
|
97
188
|
const named = nameRule(names);
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.replace(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
189
|
+
const known = makeRedactor(domains);
|
|
190
|
+
const redactText = (text) => {
|
|
191
|
+
// The names go first: a hyphenated or spaced name must be masked whole before the path rules take its first word.
|
|
192
|
+
const masked = known(rawRules(text));
|
|
193
|
+
return (named === undefined ? masked : masked.replace(named, '<user>'))
|
|
194
|
+
.replace(URL_HOST, '$1<host>')
|
|
195
|
+
.replace(INTERNAL_NAME, '<host>')
|
|
196
|
+
.replace(IPV4, '<host>')
|
|
197
|
+
.replace(IPV6, '<host>')
|
|
198
|
+
.replace(UNIX_HOME, '$1<user>')
|
|
199
|
+
.replace(WINDOWS_HOME, '$1<user>')
|
|
200
|
+
.replace(ENCODED_HOME, '$1<user>')
|
|
201
|
+
.replace(PROMPT, '<user>@<host>')
|
|
202
|
+
.replace(ENVIRONMENT, '$1<env>')
|
|
203
|
+
.replace(LOWER_ENVIRONMENT, '$1<env>');
|
|
204
|
+
};
|
|
205
|
+
// A request blob repeats messages other lines hold, so what those lines carry is collected before any line is marked.
|
|
206
|
+
const repeated = new Set();
|
|
207
|
+
const whole = parseContainer(body);
|
|
208
|
+
for (const parsed of whole !== undefined ? [whole] : body.split('\n').map(parseContainer))
|
|
209
|
+
if (parsed !== undefined && !Array.isArray(parsed))
|
|
210
|
+
learnInjected(parsed, repeated);
|
|
211
|
+
const redacted = redactDecoded(body, redactText, injectedRule(injectedKey, repeated));
|
|
110
212
|
return { text: redacted, masked: Math.max(0, placeholders(redacted) - placeholders(body)) };
|
|
111
213
|
};
|
|
112
214
|
/** 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 = '
|
|
215
|
+
export const REDACTION_VERSION = '3';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Store } from './store.js';
|
|
2
|
+
/** Where the nearest later reversion ran: in the prompt that did the work, in a later prompt of that session, or in a later session of the same repository. */
|
|
3
|
+
export type Reversion = 'same-prompt' | 'later-prompt' | 'later-session';
|
|
4
|
+
/**
|
|
5
|
+
* Reads every main-line call once: the clean shell calls' commits, revert words (read segments only, since a segment whose result the call's status does not report (`read` false) could have failed behind another command), and
|
|
6
|
+
* restored paths; and every edit that did not fail, with its digests. A subagent's call is not on the main line, so it is not read. Returns the nearest reversion of a prompt's work, or null for none found.
|
|
7
|
+
*/
|
|
8
|
+
export declare const reversionsOf: (store: Store) => ((session: string, index: number | null) => Reversion | null);
|