@manyoars/oars 0.2.0 → 0.3.0
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 +132 -28
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +6 -3
- package/dist/cli.js +367 -88
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.js +2 -2
- package/dist/errors.d.ts +13 -13
- package/dist/errors.js +28 -11
- package/dist/gate/checks.d.ts +68 -0
- package/dist/gate/checks.js +584 -0
- package/dist/gate/command.d.ts +14 -0
- package/dist/gate/command.js +61 -0
- package/dist/gate/hook-fill-main.d.ts +1 -0
- package/dist/gate/hook-fill-main.js +16 -0
- package/dist/gate/hook-fill.d.ts +38 -0
- package/dist/gate/hook-fill.js +94 -0
- package/dist/gate/hook-main.d.ts +1 -0
- package/dist/gate/hook-main.js +35 -0
- package/dist/gate/hook-process.d.ts +14 -0
- package/dist/gate/hook-process.js +64 -0
- package/dist/gate/hook.d.ts +46 -0
- package/dist/gate/hook.js +256 -0
- package/dist/gate/install.d.ts +25 -0
- package/dist/gate/install.js +126 -0
- package/dist/gate/local-bin.d.ts +8 -0
- package/dist/gate/local-bin.js +36 -0
- package/dist/gate/notes.d.ts +19 -0
- package/dist/gate/notes.js +27 -0
- package/dist/gate/novelty.d.ts +35 -0
- package/dist/gate/novelty.js +99 -0
- package/dist/gate/policy.d.ts +25 -0
- package/dist/gate/policy.js +73 -0
- package/dist/gate/record.d.ts +120 -0
- package/dist/gate/record.js +125 -0
- package/dist/gate/registry-state.d.ts +10 -0
- package/dist/gate/registry-state.js +30 -0
- package/dist/gate/registry.d.ts +56 -0
- package/dist/gate/registry.js +94 -0
- package/dist/gate/replay.d.ts +20 -0
- package/dist/gate/replay.js +257 -0
- package/dist/gate/report.d.ts +50 -0
- package/dist/gate/report.js +89 -0
- package/dist/gate/scope.d.ts +7 -0
- package/dist/gate/scope.js +27 -0
- package/dist/gate/transcripts.d.ts +68 -0
- package/dist/gate/transcripts.js +232 -0
- package/dist/hook-bin.d.ts +1 -0
- package/dist/hook-bin.js +25 -0
- package/dist/issuer.d.ts +7 -1
- package/dist/issuer.js +18 -1
- package/dist/main.d.ts +1 -0
- package/dist/main.js +64 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +351 -0
- package/dist/sessions/allow.d.ts +14 -0
- package/dist/sessions/allow.js +51 -0
- package/dist/sessions/archive.d.ts +11 -0
- package/dist/sessions/archive.js +38 -0
- package/dist/sessions/audit.d.ts +42 -0
- package/dist/sessions/audit.js +104 -0
- package/dist/sessions/baseline.d.ts +62 -0
- package/dist/sessions/baseline.js +188 -0
- package/dist/sessions/capture-runtime.d.ts +10 -0
- package/dist/sessions/capture-runtime.js +12 -0
- package/dist/sessions/cell-increments.d.ts +43 -0
- package/dist/sessions/cell-increments.js +209 -0
- package/dist/sessions/cells.d.ts +79 -0
- package/dist/sessions/cells.js +181 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +679 -0
- package/dist/sessions/command.d.ts +117 -0
- package/dist/sessions/command.js +373 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/cursor.d.ts +24 -0
- package/dist/sessions/cursor.js +324 -0
- package/dist/sessions/derive.d.ts +57 -0
- package/dist/sessions/derive.js +320 -0
- package/dist/sessions/exclusion.d.ts +81 -0
- package/dist/sessions/exclusion.js +353 -0
- package/dist/sessions/extract.d.ts +179 -0
- package/dist/sessions/extract.js +1774 -0
- package/dist/sessions/fold.d.ts +17 -0
- package/dist/sessions/fold.js +71 -0
- package/dist/sessions/frame.d.ts +24 -0
- package/dist/sessions/frame.js +95 -0
- package/dist/sessions/gemini.d.ts +37 -0
- package/dist/sessions/gemini.js +613 -0
- package/dist/sessions/gold-arms.d.ts +67 -0
- package/dist/sessions/gold-arms.js +172 -0
- package/dist/sessions/gold.d.ts +197 -0
- package/dist/sessions/gold.js +463 -0
- package/dist/sessions/graph.d.ts +12 -0
- package/dist/sessions/graph.js +34 -0
- package/dist/sessions/grok.d.ts +36 -0
- package/dist/sessions/grok.js +421 -0
- package/dist/sessions/hook-calls.d.ts +3 -0
- package/dist/sessions/hook-calls.js +47 -0
- package/dist/sessions/hook-index.d.ts +53 -0
- package/dist/sessions/hook-index.js +157 -0
- package/dist/sessions/hooks.d.ts +13 -0
- package/dist/sessions/hooks.js +170 -0
- package/dist/sessions/identity.d.ts +22 -0
- package/dist/sessions/identity.js +101 -0
- package/dist/sessions/install.d.ts +54 -0
- package/dist/sessions/install.js +201 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +6 -0
- package/dist/sessions/mcp.js +153 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +43 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/package-resolve.d.ts +30 -0
- package/dist/sessions/package-resolve.js +183 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pool-command.d.ts +44 -0
- package/dist/sessions/pool-command.js +542 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-outbox.d.ts +63 -0
- package/dist/sessions/pool-outbox.js +260 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-wire.d.ts +225 -0
- package/dist/sessions/pool-wire.js +620 -0
- package/dist/sessions/pool.d.ts +32 -0
- package/dist/sessions/pool.js +164 -0
- package/dist/sessions/projection.d.ts +75 -0
- package/dist/sessions/projection.js +165 -0
- package/dist/sessions/public-vocabulary.d.ts +73 -0
- package/dist/sessions/public-vocabulary.js +302 -0
- package/dist/sessions/query.d.ts +111 -0
- package/dist/sessions/query.js +246 -0
- package/dist/sessions/read-adoption.d.ts +2 -0
- package/dist/sessions/read-adoption.js +27 -0
- package/dist/sessions/read-call-shape.d.ts +2 -0
- package/dist/sessions/read-call-shape.js +25 -0
- package/dist/sessions/read-change.d.ts +2 -0
- package/dist/sessions/read-change.js +30 -0
- package/dist/sessions/read-cost.d.ts +4 -0
- package/dist/sessions/read-cost.js +37 -0
- package/dist/sessions/read-day-move.d.ts +3 -0
- package/dist/sessions/read-day-move.js +65 -0
- package/dist/sessions/read-fit.d.ts +7 -0
- package/dist/sessions/read-fit.js +41 -0
- package/dist/sessions/read-interaction.d.ts +5 -0
- package/dist/sessions/read-interaction.js +62 -0
- package/dist/sessions/read-lead-lag.d.ts +15 -0
- package/dist/sessions/read-lead-lag.js +81 -0
- package/dist/sessions/read-live.d.ts +6 -0
- package/dist/sessions/read-live.js +38 -0
- package/dist/sessions/read-method.d.ts +2 -0
- package/dist/sessions/read-method.js +29 -0
- package/dist/sessions/read-next-step.d.ts +2 -0
- package/dist/sessions/read-next-step.js +35 -0
- package/dist/sessions/read-order.d.ts +54 -0
- package/dist/sessions/read-order.js +62 -0
- package/dist/sessions/read-readable.d.ts +8 -0
- package/dist/sessions/read-readable.js +24 -0
- package/dist/sessions/read-recovery.d.ts +20 -0
- package/dist/sessions/read-recovery.js +97 -0
- package/dist/sessions/read-rows.d.ts +13 -0
- package/dist/sessions/read-rows.js +47 -0
- package/dist/sessions/read-substitution.d.ts +5 -0
- package/dist/sessions/read-substitution.js +51 -0
- package/dist/sessions/read-support.d.ts +48 -0
- package/dist/sessions/read-support.js +73 -0
- package/dist/sessions/read-tool-day.d.ts +2 -0
- package/dist/sessions/read-tool-day.js +26 -0
- package/dist/sessions/read-version-boundary.d.ts +31 -0
- package/dist/sessions/read-version-boundary.js +95 -0
- package/dist/sessions/read.d.ts +79 -0
- package/dist/sessions/read.js +84 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-object.d.ts +137 -0
- package/dist/sessions/recall-object.js +77 -0
- package/dist/sessions/recall-pool.d.ts +19 -0
- package/dist/sessions/recall-pool.js +96 -0
- package/dist/sessions/recall-push.d.ts +26 -0
- package/dist/sessions/recall-push.js +95 -0
- package/dist/sessions/recall-reads.d.ts +117 -0
- package/dist/sessions/recall-reads.js +410 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/redact.d.ts +6 -0
- package/dist/sessions/redact.js +54 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +644 -0
- package/dist/sessions/schema.js +148 -0
- package/dist/sessions/session-shape.d.ts +73 -0
- package/dist/sessions/session-shape.js +342 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +159 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +297 -0
- package/dist/sessions/store.js +1478 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +25 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/verify.d.ts +53 -0
- package/dist/sessions/verify.js +286 -0
- package/dist/sessions/vocab-load.d.ts +26 -0
- package/dist/sessions/vocab-load.js +61 -0
- package/dist/sessions/vscode-copilot.d.ts +38 -0
- package/dist/sessions/vscode-copilot.js +477 -0
- package/package.json +4 -5
- package/vocab/crates.json +1 -0
- package/vocab/go.json +1 -0
- package/vocab/harness.json +1 -0
- package/vocab/homebrew.json +1 -0
- package/vocab/manifest.json +1 -0
- package/vocab/maven.json +1 -0
- package/vocab/models.json +1 -0
- package/vocab/npm.json +1 -0
- package/vocab/programs.json +1 -0
- package/vocab/pypi.json +1 -0
- package/vocab/rubygems.json +1 -0
- package/dist/commands/deploy-input.d.ts +0 -38
- package/dist/commands/deploy-input.js +0 -150
- package/dist/commands/deploy.d.ts +0 -22
- package/dist/commands/deploy.js +0 -124
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -18
- package/dist/commands/templates.d.ts +0 -11
- package/dist/commands/templates.js +0 -32
- package/dist/commands/test-charge.d.ts +0 -11
- package/dist/commands/test-charge.js +0 -34
- package/dist/journal.d.ts +0 -26
- package/dist/journal.js +0 -58
- package/parity/deploy-plans/paid-api.json +0 -34
- package/parity/deploy-plans/per-task-agent.json +0 -34
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// What a recall led to (recall spec Sections 2.7 and 4). A pure function of `recalls.jsonl` and the capture records, computed at derive and never captured live, so
|
|
2
|
+
// `recalls.jsonl` stays append-only and a change here needs no re-deposit. Held recalls get a consequence too: they are the baseline a shown recall reads against. Only a
|
|
3
|
+
// shown recall touches the graph, because a held one reached nobody: it adds no `recall.*` step and exposes no cell.
|
|
4
|
+
import { motifsOf, stepsOfCall } from './motif.js';
|
|
5
|
+
import { bucketOf, floorOf } from './pool-wire.js';
|
|
6
|
+
import { harnessOf } from './schema.js';
|
|
7
|
+
/** A consequence window ends at the prompt's end or after this many calls (spec Section 2.7). */
|
|
8
|
+
export const WINDOW_CALLS = 20;
|
|
9
|
+
const callKey = (tool) => `${tool.session}#${String(tool.n)}`;
|
|
10
|
+
const promptKey = (session, index) => `${session}#${String(index)}`;
|
|
11
|
+
/**
|
|
12
|
+
* Each session's main line, in order. A hook inside a subagent reports the parent's session id, so a subagent's push is placed on the parent's line by its time; that is
|
|
13
|
+
* a weak spot of the window, not of the arm.
|
|
14
|
+
*/
|
|
15
|
+
const mainLines = (store) => {
|
|
16
|
+
const lines = new Map();
|
|
17
|
+
for (const tool of store.tools) {
|
|
18
|
+
if (tool.side)
|
|
19
|
+
continue;
|
|
20
|
+
const line = lines.get(tool.session);
|
|
21
|
+
if (line === undefined)
|
|
22
|
+
lines.set(tool.session, [tool]);
|
|
23
|
+
else
|
|
24
|
+
line.push(tool);
|
|
25
|
+
}
|
|
26
|
+
for (const line of lines.values())
|
|
27
|
+
line.sort((a, b) => a.n - b.n);
|
|
28
|
+
return lines;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The first call stamped after the recall. A call's line is written before its pre-tool hook runs, and a hook's text reaches the model after that call was already decided, so
|
|
32
|
+
* the call a push fired on is behind it and the window opens on the next decision.
|
|
33
|
+
*/
|
|
34
|
+
const positionOf = (line, at) => {
|
|
35
|
+
const next = line.findIndex((tool) => tool.ts !== null && tool.ts > at);
|
|
36
|
+
return next < 0 ? line.length : next;
|
|
37
|
+
};
|
|
38
|
+
const outcomeOf = (prompt) => ({
|
|
39
|
+
errors: prompt?.errors ?? null,
|
|
40
|
+
verify: prompt?.verify ?? null,
|
|
41
|
+
stopped: prompt?.stopped ?? null,
|
|
42
|
+
commits: prompt?.commits ?? null,
|
|
43
|
+
redo: prompt?.redo ?? null,
|
|
44
|
+
});
|
|
45
|
+
const consequenceOf = (recall, session, line, position, harness, prompts) => {
|
|
46
|
+
const prompt = (line[position - 1] ?? line[position])?.prompt ?? null;
|
|
47
|
+
const calls = [];
|
|
48
|
+
let end = 'session';
|
|
49
|
+
for (const tool of line.slice(position)) {
|
|
50
|
+
if (calls.length === WINDOW_CALLS) {
|
|
51
|
+
end = 'calls';
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (tool.prompt !== prompt) {
|
|
55
|
+
end = 'prompt';
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
calls.push(tool);
|
|
59
|
+
}
|
|
60
|
+
const steps = calls.flatMap((tool) => stepsOfCall(tool, harness));
|
|
61
|
+
const names = new Set(recall.answer.names);
|
|
62
|
+
const measured = calls.flatMap((tool) => tool.steps ?? []).flatMap((step) => step.cost.tokens ?? []);
|
|
63
|
+
return {
|
|
64
|
+
window: { from: position, to: position + calls.length, calls: calls.length, end },
|
|
65
|
+
// Shape level only: it is the ID that joins across accounts, and named windows would multiply a row that exists per recall.
|
|
66
|
+
motifs: [...new Set(motifsOf(steps).flatMap(({ motif }) => (motif.level === 'shape' ? [motif.motifId] : [])))],
|
|
67
|
+
followed: steps.some((step) => names.has(step.action)),
|
|
68
|
+
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))),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/** 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. */
|
|
73
|
+
export const deriveConsequences = (store, recalls) => {
|
|
74
|
+
const lines = mainLines(store);
|
|
75
|
+
const prompts = new Map(store.prompts.map((prompt) => [promptKey(prompt.session, prompt.index), prompt]));
|
|
76
|
+
const sessions = new Map(store.sessions.map((session) => [session.session, session]));
|
|
77
|
+
return recalls.map((recall) => {
|
|
78
|
+
const session = recall.sid === null ? undefined : sessions.get(recall.sid);
|
|
79
|
+
if (session === undefined)
|
|
80
|
+
return recall;
|
|
81
|
+
const line = lines.get(session.session) ?? [];
|
|
82
|
+
const position = positionOf(line, recall.at);
|
|
83
|
+
return { ...recall, n: position, consequence: consequenceOf(recall, session.session, line, position, harnessOf(session), prompts) };
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
export const NO_RECALLS = {
|
|
87
|
+
place: (_tool, steps) => steps.map((step) => ({ step, exposed: false })),
|
|
88
|
+
exposedPrompt: () => false,
|
|
89
|
+
};
|
|
90
|
+
const pushTo = (map, key, step) => {
|
|
91
|
+
const found = map.get(key);
|
|
92
|
+
if (found === undefined)
|
|
93
|
+
map.set(key, [step]);
|
|
94
|
+
else
|
|
95
|
+
found.push(step);
|
|
96
|
+
};
|
|
97
|
+
/** The step enters a cell after the store is floored, so its own count is floored here: no cell holds a number finer than its bucket's floor (rule 5). */
|
|
98
|
+
const recallStep = (recall) => ({
|
|
99
|
+
step: { action: recall.mode === 'pull' ? 'recall.pull' : 'recall.push', identity: null, status: 'ok', tokens: floorOf('tokens', bucketOf('tokens', recall.answer.tokens)), durationMs: 0 },
|
|
100
|
+
exposed: true,
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* Takes recalls that have been through `deriveConsequences`. An MCP pull has no session id, so the only step it has is the one in its transcript; a pull that names its session
|
|
104
|
+
* (`--session`) has no MCP call, so its step is this one.
|
|
105
|
+
*/
|
|
106
|
+
export const recallGraphOf = (store, recalls) => {
|
|
107
|
+
const lines = mainLines(store);
|
|
108
|
+
const exposedCalls = new Set();
|
|
109
|
+
const exposedPrompts = new Set();
|
|
110
|
+
const before = new Map();
|
|
111
|
+
const after = new Map();
|
|
112
|
+
for (const recall of recalls) {
|
|
113
|
+
const line = recall.sid === null ? undefined : lines.get(recall.sid);
|
|
114
|
+
if (recall.arm === 'held' || recall.consequence === undefined || line === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
const { from, to } = recall.consequence.window;
|
|
117
|
+
// A shown miss showed no evidence, so its window is not an exposure; the step stays, since the pull happened.
|
|
118
|
+
if (recall.answer.kind !== 'miss') {
|
|
119
|
+
for (const tool of line.slice(from, to)) {
|
|
120
|
+
exposedCalls.add(callKey(tool));
|
|
121
|
+
exposedPrompts.add(promptKey(tool.session, tool.prompt));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const previous = line[from - 1];
|
|
125
|
+
const first = line[from];
|
|
126
|
+
if (previous !== undefined)
|
|
127
|
+
pushTo(after, callKey(previous), recallStep(recall));
|
|
128
|
+
else if (first !== undefined)
|
|
129
|
+
pushTo(before, callKey(first), recallStep(recall));
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
place: (tool, steps) => {
|
|
133
|
+
const key = callKey(tool);
|
|
134
|
+
const exposed = exposedCalls.has(key);
|
|
135
|
+
return [...(before.get(key) ?? []), ...steps.map((step) => ({ step, exposed })), ...(after.get(key) ?? [])];
|
|
136
|
+
},
|
|
137
|
+
exposedPrompt: (session, index) => exposedPrompts.has(promptKey(session, index)),
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const sharesACall = (one, other) => Math.max(one.from, other.from) < Math.min(one.to, other.to);
|
|
141
|
+
/** Takes recalls that have been through `deriveConsequences`; `day` is a UTC day, so one day of hooks reads as one set of rows. A shown window from any day can overlap a held one. */
|
|
142
|
+
export const recallCounts = (recalls, day) => {
|
|
143
|
+
const rows = new Map();
|
|
144
|
+
const shown = new Map();
|
|
145
|
+
for (const recall of recalls) {
|
|
146
|
+
if (recall.arm === 'shown' && recall.consequence !== undefined)
|
|
147
|
+
shown.set(recall.sid, [...(shown.get(recall.sid) ?? []), recall.consequence.window]);
|
|
148
|
+
}
|
|
149
|
+
for (const recall of recalls) {
|
|
150
|
+
if (day !== undefined && !recall.at.startsWith(day))
|
|
151
|
+
continue;
|
|
152
|
+
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, followed: 0, windowCalls: 0, windowTokens: null, overlapped: 0 }, sessions: new Set() };
|
|
154
|
+
rows.set(key, held);
|
|
155
|
+
const { row, sessions } = held;
|
|
156
|
+
row.recalls += 1;
|
|
157
|
+
row.tokens += recall.answer.tokens;
|
|
158
|
+
if (recall.sid !== null)
|
|
159
|
+
sessions.add(recall.sid);
|
|
160
|
+
row.sessions = sessions.size;
|
|
161
|
+
const { consequence } = recall;
|
|
162
|
+
if (consequence === undefined)
|
|
163
|
+
continue;
|
|
164
|
+
row.windows += 1;
|
|
165
|
+
if (consequence.followed)
|
|
166
|
+
row.followed += 1;
|
|
167
|
+
row.windowCalls += consequence.window.calls;
|
|
168
|
+
if (consequence.cost.tokens !== null)
|
|
169
|
+
row.windowTokens = (row.windowTokens ?? 0) + consequence.cost.tokens;
|
|
170
|
+
if (recall.arm === 'held' && (shown.get(recall.sid) ?? []).some((window) => sharesACall(consequence.window, window)))
|
|
171
|
+
row.overlapped += 1;
|
|
172
|
+
}
|
|
173
|
+
return [...rows.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([, { row }]) => row);
|
|
174
|
+
};
|
|
175
|
+
export const formatRecallCounts = (rows) => rows.length === 0
|
|
176
|
+
? 'No recalls in that span.'
|
|
177
|
+
: 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.followed)} followed, ${String(row.windowCalls)} calls, ${row.windowTokens === null ? 'window tokens unmeasured' : `${String(row.windowTokens)} window tokens`}, ${String(row.overlapped)} overlapped`)
|
|
179
|
+
.join('\n');
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Frame } from './schema.js';
|
|
2
|
+
export declare const recallsPath: (storeDir: string) => string;
|
|
3
|
+
/** The share of pushes held back (spec Section 8, open parameter). */
|
|
4
|
+
export declare const HOLDBACK_RATE = 0.1;
|
|
5
|
+
export declare const RECALL_CLASSES: {
|
|
6
|
+
readonly A: readonly ["sid", "at", "query.text"];
|
|
7
|
+
readonly P: readonly ["id", "n", "frame", "mode", "query.form", "query.prefix", "query.entity", "query.family", "query.slice", "poolVersion", "arm", "answer", "evidence", "consequence"];
|
|
8
|
+
};
|
|
9
|
+
export type RecallMode = 'pull' | 'push';
|
|
10
|
+
export type RecallArm = 'shown' | 'held';
|
|
11
|
+
export type RecallForm = 'pattern' | 'entity' | 'question' | 'text';
|
|
12
|
+
/** Which frames a read covers. Each name is held to the public vocabulary before it is shown. */
|
|
13
|
+
export interface RecallSlice {
|
|
14
|
+
harnessVersion?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
tool?: string;
|
|
17
|
+
cls?: string;
|
|
18
|
+
sinceDays?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface RecallQueryObject {
|
|
21
|
+
form: RecallForm;
|
|
22
|
+
/** What the asker's steps so far name: motif IDs, stored read IDs, or the shape and signature hashes `recall-reads.ts` computes. */
|
|
23
|
+
prefix?: string[];
|
|
24
|
+
/** A public name: package, tool, MCP server, or model. */
|
|
25
|
+
entity?: string;
|
|
26
|
+
family?: string;
|
|
27
|
+
slice?: RecallSlice;
|
|
28
|
+
/** Class A, local only. */
|
|
29
|
+
text?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface RecallAnswer {
|
|
32
|
+
kind: 'hit' | 'miss';
|
|
33
|
+
/** Read IDs, in the order shown. */
|
|
34
|
+
reads: string[];
|
|
35
|
+
/** The size of what was injected, or of what a held recall would have injected. */
|
|
36
|
+
tokens: number;
|
|
37
|
+
/** What the asker saw, or for a held recall would have seen. It holds structured fields only (`recall-render.ts`). */
|
|
38
|
+
rendered: string;
|
|
39
|
+
/** The closed names a follow can match: canonical actions, in the order shown. */
|
|
40
|
+
names: string[];
|
|
41
|
+
}
|
|
42
|
+
export type DeltaAxis = 'same' | 'differs' | 'unknown';
|
|
43
|
+
/** How far the evidence sits from the asker, per axis, never collapsed into a score (spec Section 3). */
|
|
44
|
+
export interface RecallDelta {
|
|
45
|
+
harness: DeltaAxis;
|
|
46
|
+
/** The asker's version minus the evidence's newest, when only the last component differs; null when that does not read as a count. */
|
|
47
|
+
harnessVersions: number | null;
|
|
48
|
+
model: DeltaAxis;
|
|
49
|
+
/** How many of the evidence's models are not the asker's; null when the asker's is unknown. */
|
|
50
|
+
modelVersions: number | null;
|
|
51
|
+
platform: DeltaAxis;
|
|
52
|
+
shell: DeltaAxis;
|
|
53
|
+
stack: DeltaAxis;
|
|
54
|
+
/** No record states a tool's version yet. */
|
|
55
|
+
toolVersion: number | null;
|
|
56
|
+
ageDays: number | null;
|
|
57
|
+
}
|
|
58
|
+
export interface FrameRange {
|
|
59
|
+
harnesses: string[];
|
|
60
|
+
harnessVersions: string[];
|
|
61
|
+
models: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface RecallEvidence {
|
|
64
|
+
/** A read, cell, or recall ID. Never another account's session. */
|
|
65
|
+
ref: string;
|
|
66
|
+
frameRange: FrameRange;
|
|
67
|
+
firstDay: string | null;
|
|
68
|
+
lastDay: string | null;
|
|
69
|
+
n: number;
|
|
70
|
+
clusters: number;
|
|
71
|
+
delta: RecallDelta;
|
|
72
|
+
}
|
|
73
|
+
/** The outcome signals of the window's prompt, held separately: no composite (spec Section 2.7). */
|
|
74
|
+
export interface RecallOutcome {
|
|
75
|
+
errors: number | null;
|
|
76
|
+
verify: string | null;
|
|
77
|
+
stopped: string | null;
|
|
78
|
+
commits: number | null;
|
|
79
|
+
redo: boolean | null;
|
|
80
|
+
}
|
|
81
|
+
/** Derived from the capture records, never captured live. */
|
|
82
|
+
export interface RecallConsequence {
|
|
83
|
+
/** The calls the window holds, by position, and why it ended. */
|
|
84
|
+
window: {
|
|
85
|
+
from: number;
|
|
86
|
+
to: number;
|
|
87
|
+
calls: number;
|
|
88
|
+
end: 'prompt' | 'calls' | 'session';
|
|
89
|
+
};
|
|
90
|
+
motifs: string[];
|
|
91
|
+
followed: boolean;
|
|
92
|
+
/** `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. */
|
|
93
|
+
cost: {
|
|
94
|
+
tokens: number | null;
|
|
95
|
+
durationMs: number;
|
|
96
|
+
};
|
|
97
|
+
outcome: RecallOutcome;
|
|
98
|
+
}
|
|
99
|
+
export interface Recall {
|
|
100
|
+
id: string;
|
|
101
|
+
/** The session the asker ran in; null for a pull made outside one, which no consequence can be read for. Class A. */
|
|
102
|
+
sid: string | null;
|
|
103
|
+
/** Class A. */
|
|
104
|
+
at: string;
|
|
105
|
+
/** The asker's position among its calls. A hook cannot know it without reading the transcript, so derive fills it from `at`. */
|
|
106
|
+
n: number | null;
|
|
107
|
+
frame: Frame | null;
|
|
108
|
+
mode: RecallMode;
|
|
109
|
+
query: RecallQueryObject;
|
|
110
|
+
/** The snapshot answered from: the deposit time of the store or index, so "older than the recall" is a comparison. */
|
|
111
|
+
poolVersion: string;
|
|
112
|
+
arm: RecallArm;
|
|
113
|
+
answer: RecallAnswer;
|
|
114
|
+
evidence: RecallEvidence[];
|
|
115
|
+
consequence?: RecallConsequence;
|
|
116
|
+
}
|
|
117
|
+
export interface RecallInput {
|
|
118
|
+
sid: string | null;
|
|
119
|
+
at: string;
|
|
120
|
+
frame: Frame | null;
|
|
121
|
+
mode: RecallMode;
|
|
122
|
+
query: RecallQueryObject;
|
|
123
|
+
poolVersion: string;
|
|
124
|
+
arm: RecallArm;
|
|
125
|
+
reads: readonly string[];
|
|
126
|
+
rendered: string;
|
|
127
|
+
names: readonly string[];
|
|
128
|
+
evidence: readonly RecallEvidence[];
|
|
129
|
+
}
|
|
130
|
+
export declare const makeRecall: (input: RecallInput) => Recall;
|
|
131
|
+
/** `held` for the hash's first 32 bits under the rate. One session asking from one prefix against one pool always lands on one arm. */
|
|
132
|
+
export declare const armOf: (sid: string, prefix: readonly string[], poolVersion: string, rate?: number) => RecallArm;
|
|
133
|
+
/** Evidence must come from a pool older than the recall (spec Section 3): that one rule is why the commons holds no cycles. */
|
|
134
|
+
export declare const evidencePredates: (recall: Recall) => boolean;
|
|
135
|
+
export declare const appendRecall: (storeDir: string, recall: Recall) => Promise<void>;
|
|
136
|
+
export declare const parseRecalls: (text: string) => Recall[];
|
|
137
|
+
export declare const readRecalls: (storeDir: string) => Promise<Recall[]>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The recall object (recall spec Section 2.7): one event in the asker's graph, with edges back to the evidence it drew on and forward to what the asker
|
|
2
|
+
// did next. A pull and a push are the same object, and both land in `recalls.jsonl`, append-only, whether or not anything was injected.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
const RECALLS_FILE = 'recalls.jsonl';
|
|
7
|
+
export const recallsPath = (storeDir) => join(storeDir, RECALLS_FILE);
|
|
8
|
+
/** The share of pushes held back (spec Section 8, open parameter). */
|
|
9
|
+
export const HOLDBACK_RATE = 0.1;
|
|
10
|
+
export const RECALL_CLASSES = {
|
|
11
|
+
A: ['sid', 'at', 'query.text'],
|
|
12
|
+
P: ['id', 'n', 'frame', 'mode', 'query.form', 'query.prefix', 'query.entity', 'query.family', 'query.slice', 'poolVersion', 'arm', 'answer', 'evidence', 'consequence'],
|
|
13
|
+
};
|
|
14
|
+
/** Characters per token, the rule of thumb: the size is a count for comparing arms, not a bill. */
|
|
15
|
+
const CHARS_PER_TOKEN = 4;
|
|
16
|
+
const hash = (parts) => createHash('sha256').update(JSON.stringify(parts)).digest('hex');
|
|
17
|
+
export const makeRecall = (input) => ({
|
|
18
|
+
id: hash([input.sid, input.at, input.mode, input.query, input.poolVersion]).slice(0, 16),
|
|
19
|
+
sid: input.sid,
|
|
20
|
+
at: input.at,
|
|
21
|
+
n: null,
|
|
22
|
+
frame: input.frame,
|
|
23
|
+
mode: input.mode,
|
|
24
|
+
query: input.query,
|
|
25
|
+
poolVersion: input.poolVersion,
|
|
26
|
+
arm: input.arm,
|
|
27
|
+
answer: {
|
|
28
|
+
kind: input.reads.length === 0 ? 'miss' : 'hit',
|
|
29
|
+
reads: [...input.reads],
|
|
30
|
+
tokens: Math.ceil(input.rendered.length / CHARS_PER_TOKEN),
|
|
31
|
+
rendered: input.rendered,
|
|
32
|
+
names: [...input.names],
|
|
33
|
+
},
|
|
34
|
+
evidence: [...input.evidence],
|
|
35
|
+
});
|
|
36
|
+
const SCALE = 0x1_0000_0000;
|
|
37
|
+
/** `held` for the hash's first 32 bits under the rate. One session asking from one prefix against one pool always lands on one arm. */
|
|
38
|
+
export const armOf = (sid, prefix, poolVersion, rate = HOLDBACK_RATE) => Number.parseInt(hash([sid, prefix, poolVersion]).slice(0, 8), 16) / SCALE < rate ? 'held' : 'shown';
|
|
39
|
+
/** Evidence must come from a pool older than the recall (spec Section 3): that one rule is why the commons holds no cycles. */
|
|
40
|
+
export const evidencePredates = (recall) => {
|
|
41
|
+
const day = recall.at.slice(0, 10);
|
|
42
|
+
return recall.poolVersion <= recall.at && recall.evidence.every((item) => item.lastDay === null || item.lastDay <= day);
|
|
43
|
+
};
|
|
44
|
+
export const appendRecall = async (storeDir, recall) => {
|
|
45
|
+
await mkdir(storeDir, { recursive: true });
|
|
46
|
+
await appendFile(recallsPath(storeDir), `${JSON.stringify(recall)}\n`, { mode: 0o600 });
|
|
47
|
+
};
|
|
48
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
49
|
+
/** The file is ours, so a line's shape is checked only as far as telling a recall from a torn or foreign line. */
|
|
50
|
+
const isRecall = (value) => isRecord(value) && typeof value.id === 'string' && typeof value.at === 'string' && isRecord(value.answer) && isRecord(value.query);
|
|
51
|
+
export const parseRecalls = (text) => {
|
|
52
|
+
const recalls = [];
|
|
53
|
+
for (const line of text.split('\n')) {
|
|
54
|
+
if (line === '')
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(line);
|
|
58
|
+
if (isRecall(parsed))
|
|
59
|
+
recalls.push(parsed);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// A torn line is skipped.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return recalls;
|
|
66
|
+
};
|
|
67
|
+
export const readRecalls = async (storeDir) => {
|
|
68
|
+
let text = '';
|
|
69
|
+
try {
|
|
70
|
+
text = await readFile(recallsPath(storeDir), 'utf8');
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
return parseRecalls(text);
|
|
77
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Pool } from './cells.js';
|
|
2
|
+
import type { Read } from './read.js';
|
|
3
|
+
import type { RecallRead } from './recall-reads.js';
|
|
4
|
+
export interface CurrentPool {
|
|
5
|
+
pool: Pool;
|
|
6
|
+
reads: readonly Read[];
|
|
7
|
+
}
|
|
8
|
+
/** Each answer under its stored read's ID, where the pool holds one. */
|
|
9
|
+
export declare const resolveReads: (reads: readonly RecallRead[], stored: readonly Read[]) => RecallRead[];
|
|
10
|
+
/** The answers to a question about a family only the pool holds, best first. */
|
|
11
|
+
export declare const readsOfFamily: (family: string, current: CurrentPool) => RecallRead[];
|
|
12
|
+
/**
|
|
13
|
+
* What a prefix names in the pool: a stored read by its ID, or a motif by its ID. A stored read with a stand-in goes back through it, because the store's own counts answer those;
|
|
14
|
+
* `standIns` holds those hashes for the caller to match, and `reads` every other answer.
|
|
15
|
+
*/
|
|
16
|
+
export declare const namedByPrefix: (prefix: readonly string[], current: CurrentPool) => {
|
|
17
|
+
standIns: string[];
|
|
18
|
+
reads: RecallRead[];
|
|
19
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// What ties a recall's answer to the stored read of the current pool. A store-wide count answers the asker, and the read it rests on is what `lineage` can
|
|
2
|
+
// walk, so each answer carries that read's ID. Without a stored read for it, an answer keeps its stand-in hash, which `lineage` reports as evidence with no further hop.
|
|
3
|
+
import { bucketFields, cellIdOf, lastDayOfCell, UNKNOWN } from './cells.js';
|
|
4
|
+
import { motifName } from './motif.js';
|
|
5
|
+
/** The stand-in hash a stored read answers for: a recovery read's signature hash and a call-shape read's shape are the values `recall-reads.ts` computes from the store. */
|
|
6
|
+
const standInOf = (read) => {
|
|
7
|
+
if (read.family === 'recovery')
|
|
8
|
+
return read.slice.sig;
|
|
9
|
+
return read.family === 'fit' ? read.slice.shape : undefined;
|
|
10
|
+
};
|
|
11
|
+
const keyOf = (family, standIn) => `${family}/${standIn}`;
|
|
12
|
+
/** Each answer under its stored read's ID, where the pool holds one. */
|
|
13
|
+
export const resolveReads = (reads, stored) => {
|
|
14
|
+
const ids = new Map(stored.flatMap((read) => {
|
|
15
|
+
const standIn = standInOf(read);
|
|
16
|
+
return standIn === undefined ? [] : [[keyOf(read.family, standIn), read.readId]];
|
|
17
|
+
}));
|
|
18
|
+
return reads.map((read) => ({ ...read, readId: ids.get(keyOf(read.family, read.readId)) ?? read.readId }));
|
|
19
|
+
};
|
|
20
|
+
const countIn = (value) => (typeof value === 'number' ? value : 0);
|
|
21
|
+
const nextOf = (read) => {
|
|
22
|
+
const { next } = read.value;
|
|
23
|
+
if (typeof next !== 'object' || next === null)
|
|
24
|
+
return [];
|
|
25
|
+
return Object.entries(next).sort(([a, one], [b, two]) => two - one || a.localeCompare(b));
|
|
26
|
+
};
|
|
27
|
+
const FOLLOWED = 3;
|
|
28
|
+
const presentOf = (values) => [...new Set(values.filter((value) => value !== UNKNOWN && value !== ''))].sort();
|
|
29
|
+
/** A stored read as an answer. Its evidence's frames and days come from the cells it cites; a daily session count cannot be summed, so `n` is the floor those cells prove. */
|
|
30
|
+
const answerOfStored = (read, cells) => {
|
|
31
|
+
const cited = read.inputs.flatMap((input) => cells.get(input) ?? []);
|
|
32
|
+
const frames = cited.map((cell) => bucketFields(cell.frameBucket));
|
|
33
|
+
// `firstDay` over a week cell is that week's Monday: the earliest day the evidence can hold, which the evidence may not have touched.
|
|
34
|
+
const days = presentOf(cited.map((cell) => cell.day));
|
|
35
|
+
// A week cell may hold any of its seven days, so the last day the evidence can hold is the week's end, but never past the pool the read was made from: a read cannot hold a day its pool
|
|
36
|
+
// had not seen, and a recall made on the Wednesday of its evidence's week must not find that evidence dated after itself.
|
|
37
|
+
const lastDays = presentOf(cited.map((cell) => (read.asOf.day !== null && lastDayOfCell(cell) > read.asOf.day ? read.asOf.day : lastDayOfCell(cell))));
|
|
38
|
+
const isRate = read.family === 'fit';
|
|
39
|
+
const failed = countIn(read.value.failed);
|
|
40
|
+
return {
|
|
41
|
+
readId: read.readId,
|
|
42
|
+
family: read.family,
|
|
43
|
+
tool: read.slice.motif ?? read.slice.prefix ?? read.slice.class ?? read.slice.action ?? read.family,
|
|
44
|
+
keys: [],
|
|
45
|
+
n: cited.reduce((highest, cell) => Math.max(highest, cell.sessions), 0),
|
|
46
|
+
count: isRate ? failed : read.n,
|
|
47
|
+
calls: isRate ? failed + countIn(read.value.ok) : null,
|
|
48
|
+
clusters: read.clusters,
|
|
49
|
+
firstDay: days[0] ?? null,
|
|
50
|
+
lastDay: lastDays[lastDays.length - 1] ?? null,
|
|
51
|
+
harnesses: presentOf(frames.map((frame) => frame.harness)),
|
|
52
|
+
harnessVersions: [],
|
|
53
|
+
models: presentOf(frames.map((frame) => frame.model)),
|
|
54
|
+
followed: nextOf(read).slice(0, FOLLOWED),
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/** A pull can answer an agent, and an experimental read goes to no agent (`docs/sessions-store.md`), so a recall never serves one; `oars sessions read` still prints it. */
|
|
58
|
+
const isServed = (read) => read.value.label !== 'experimental';
|
|
59
|
+
const cellsOf = (pool) => new Map(pool.cells.map((cell) => [cellIdOf(cell), cell]));
|
|
60
|
+
const ranked = (reads) => [...reads].sort((a, b) => b.n - a.n || b.count - a.count || a.readId.localeCompare(b.readId));
|
|
61
|
+
/** The answers to a question about a family only the pool holds, best first. */
|
|
62
|
+
export const readsOfFamily = (family, current) => {
|
|
63
|
+
const cells = cellsOf(current.pool);
|
|
64
|
+
return ranked(current.reads.filter((read) => read.family === family && isServed(read)).map((read) => answerOfStored(read, cells)));
|
|
65
|
+
};
|
|
66
|
+
const ofMotif = (motif, reads) => {
|
|
67
|
+
const name = motifName(motif);
|
|
68
|
+
return reads.filter((read) => (read.family === 'fit' && read.slice.motif === name && read.slice.level === motif.level) || (read.family === 'next-step' && motif.level === 'shape' && read.slice.prefix === name));
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* What a prefix names in the pool: a stored read by its ID, or a motif by its ID. A stored read with a stand-in goes back through it, because the store's own counts answer those;
|
|
72
|
+
* `standIns` holds those hashes for the caller to match, and `reads` every other answer.
|
|
73
|
+
*/
|
|
74
|
+
export const namedByPrefix = (prefix, current) => {
|
|
75
|
+
const named = new Set();
|
|
76
|
+
for (const id of prefix) {
|
|
77
|
+
for (const read of current.reads)
|
|
78
|
+
if (read.readId === id && isServed(read))
|
|
79
|
+
named.add(read);
|
|
80
|
+
const motif = current.pool.motifs.get(id);
|
|
81
|
+
if (motif !== undefined)
|
|
82
|
+
for (const read of ofMotif(motif, current.reads))
|
|
83
|
+
named.add(read);
|
|
84
|
+
}
|
|
85
|
+
const cells = cellsOf(current.pool);
|
|
86
|
+
const standIns = [];
|
|
87
|
+
const reads = [];
|
|
88
|
+
for (const read of named) {
|
|
89
|
+
const standIn = standInOf(read);
|
|
90
|
+
if (standIn === undefined)
|
|
91
|
+
reads.push(answerOfStored(read, cells));
|
|
92
|
+
else
|
|
93
|
+
standIns.push(standIn);
|
|
94
|
+
}
|
|
95
|
+
return { standIns, reads: ranked(reads) };
|
|
96
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HookIndex } from './hook-index.js';
|
|
2
|
+
import { type Recall } from './recall-object.js';
|
|
3
|
+
import { type Frame } from './schema.js';
|
|
4
|
+
/** A file's last bytes; a first line this cuts in two fails to parse and is skipped by whoever reads it. */
|
|
5
|
+
export declare const tailOf: (file: string) => Promise<string>;
|
|
6
|
+
/**
|
|
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
|
+
* unknown here; `delta` says unknown for them, and derive reads the session's real frame.
|
|
9
|
+
*/
|
|
10
|
+
export declare const frameOfTail: (tail: string, platform: string, at: string) => Frame;
|
|
11
|
+
export interface PushInput {
|
|
12
|
+
storeDir: string;
|
|
13
|
+
sid: string;
|
|
14
|
+
tool: string;
|
|
15
|
+
/** The call's input key names, never their values. */
|
|
16
|
+
keys: readonly string[];
|
|
17
|
+
transcript: string | undefined;
|
|
18
|
+
platform: string;
|
|
19
|
+
at: string;
|
|
20
|
+
index: HookIndex;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The recall a call about to run gets, written before anything is injected: an exposure with no record is the one failure the arms cannot survive. Undefined when no read
|
|
24
|
+
* covers the shape, which keeps no row (no one asked, so it is not a want, and it would be a row per call), and when this session has already been told about this shape.
|
|
25
|
+
*/
|
|
26
|
+
export declare const pushRecall: (input: PushInput) => Promise<Recall | undefined>;
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
// 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
|
+
import { closedFrame, closedStack, utcDay } from './frame.js';
|
|
5
|
+
import { appendRecall, armOf, makeRecall, parseRecalls, recallsPath } from './recall-object.js';
|
|
6
|
+
import { toolShapeId } from './recall-reads.js';
|
|
7
|
+
import { evidenceOf, renderAnswer } from './recall-render.js';
|
|
8
|
+
import { harnessOf } from './schema.js';
|
|
9
|
+
/** How far back a hook looks in a growing file: enough for the session's latest lines, and a bound on what a hook on every call can cost. */
|
|
10
|
+
const TAIL_BYTES = 64 * 1024;
|
|
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
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* 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
|
+
* unknown here; `delta` says unknown for them, and derive reads the session's real frame.
|
|
52
|
+
*/
|
|
53
|
+
export const frameOfTail = (tail, platform, at) => {
|
|
54
|
+
let harnessVersion = null;
|
|
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
|
+
}
|
|
64
|
+
return closedFrame({ harness: harnessOf({}), harnessVersion, model, effort, day: utcDay(at), platform, shell: null, stack: closedStack({ languages: [], frameworks: [], size: null }) });
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The recall a call about to run gets, written before anything is injected: an exposure with no record is the one failure the arms cannot survive. Undefined when no read
|
|
68
|
+
* covers the shape, which keeps no row (no one asked, so it is not a want, and it would be a row per call), and when this session has already been told about this shape.
|
|
69
|
+
*/
|
|
70
|
+
export const pushRecall = async (input) => {
|
|
71
|
+
const shape = toolShapeId(input.tool, input.keys);
|
|
72
|
+
const read = input.index.fit?.[shape];
|
|
73
|
+
if (read === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const earlier = parseRecalls(await tailOf(recallsPath(input.storeDir)));
|
|
76
|
+
if (earlier.some((recall) => recall.mode === 'push' && recall.sid === input.sid && recall.query.prefix?.[0] === shape))
|
|
77
|
+
return undefined;
|
|
78
|
+
const frame = frameOfTail(input.transcript === undefined ? '' : await tailOf(input.transcript), input.platform, input.at);
|
|
79
|
+
const { rendered, names } = renderAnswer([read], frame);
|
|
80
|
+
const recall = makeRecall({
|
|
81
|
+
sid: input.sid,
|
|
82
|
+
at: input.at,
|
|
83
|
+
frame,
|
|
84
|
+
mode: 'push',
|
|
85
|
+
query: { form: 'pattern', prefix: [shape] },
|
|
86
|
+
poolVersion: input.index.at,
|
|
87
|
+
arm: armOf(input.sid, [shape], input.index.at),
|
|
88
|
+
reads: [read.readId],
|
|
89
|
+
rendered,
|
|
90
|
+
names,
|
|
91
|
+
evidence: [evidenceOf(read, frame)],
|
|
92
|
+
});
|
|
93
|
+
await appendRecall(input.storeDir, recall);
|
|
94
|
+
return recall;
|
|
95
|
+
};
|