@mnemom/mnemom 0.17.37 → 0.17.38-next.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/dist/commands/agent-doctor.d.ts +2 -2
- package/dist/commands/agent-doctor.js +4 -4
- package/dist/commands/agent-run.d.ts +11 -0
- package/dist/commands/agent-run.js +37 -0
- package/dist/commands/agent-setup.d.ts +11 -9
- package/dist/commands/agent-setup.js +24 -19
- package/dist/commands/agent-share-render.d.ts +5 -5
- package/dist/commands/agent-share-render.js +9 -7
- package/dist/commands/agent-share-test-issuer.d.ts +1 -1
- package/dist/commands/agent-share-test-issuer.js +1 -1
- package/dist/commands/agent.d.ts +8 -1
- package/dist/commands/agent.js +14 -3
- package/dist/commands/card.js +1 -1
- package/dist/commands/onboard.js +5 -4
- package/dist/commands/scan.d.ts +35 -8
- package/dist/commands/scan.js +134 -45
- package/dist/commands/verify-card.d.ts +25 -1
- package/dist/commands/verify-card.js +29 -2
- package/dist/commands/wrap.js +4 -3
- package/dist/index.js +8 -6
- package/dist/lib/commit-report.d.ts +7 -13
- package/dist/lib/commit-report.js +12 -5
- package/dist/lib/conduct-receipt.d.ts +8 -3
- package/dist/lib/conduct-receipt.js +29 -7
- package/dist/lib/experimental.d.ts +1 -1
- package/dist/lib/experimental.js +1 -12
- package/dist/lib/runner/executor-client.d.ts +5 -0
- package/dist/lib/runner/executor-client.js +8 -0
- package/dist/lib/runner/job.d.ts +37 -0
- package/dist/lib/runner/job.js +74 -1
- package/dist/lib/runner/phases.d.ts +7 -2
- package/dist/lib/runner/phases.js +7 -2
- package/dist/lib/runner/proxy.d.ts +26 -2
- package/dist/lib/runner/proxy.js +33 -1
- package/dist/lib/runner/record-commit.d.ts +16 -0
- package/dist/lib/runner/record-commit.js +7 -0
- package/dist/lib/scan/report-email.d.ts +19 -2
- package/dist/lib/scan/report-email.js +78 -22
- package/dist/lib/scan/scan.d.ts +2 -0
- package/dist/lib/scan/scan.js +2 -1
- package/dist/lib/scan/share.d.ts +8 -6
- package/dist/lib/scan/share.js +28 -11
- package/dist/lib/scan/spend/anthropic-usage.d.ts +20 -1
- package/dist/lib/scan/spend/anthropic-usage.js +32 -7
- package/dist/lib/scan/spend/anthropic-wif.d.ts +42 -0
- package/dist/lib/scan/spend/anthropic-wif.js +153 -0
- package/dist/lib/starter-integrity.d.ts +20 -0
- package/dist/lib/starter-integrity.js +28 -0
- package/package.json +2 -5
- package/node_modules/@mnemom/aip-verifier/LICENSE +0 -190
- package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +0 -478
- package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +0 -478
- package/node_modules/@mnemom/aip-verifier/dist/index.cjs +0 -971
- package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +0 -1
- package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +0 -330
- package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +0 -330
- package/node_modules/@mnemom/aip-verifier/dist/index.js +0 -933
- package/node_modules/@mnemom/aip-verifier/dist/index.js.map +0 -1
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +0 -879
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +0 -1
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +0 -96
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +0 -96
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +0 -875
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +0 -1
- package/node_modules/@mnemom/aip-verifier/package.json +0 -86
- package/node_modules/@noble/ed25519/LICENSE +0 -21
- package/node_modules/@noble/ed25519/README.md +0 -371
- package/node_modules/@noble/ed25519/index.d.ts +0 -391
- package/node_modules/@noble/ed25519/index.js +0 -933
- package/node_modules/@noble/ed25519/index.ts +0 -1134
- package/node_modules/@noble/ed25519/package.json +0 -54
- package/node_modules/@noble/hashes/LICENSE +0 -21
- package/node_modules/@noble/hashes/README.md +0 -478
- package/node_modules/@noble/hashes/_blake.d.ts +0 -20
- package/node_modules/@noble/hashes/_blake.js +0 -59
- package/node_modules/@noble/hashes/_md.d.ts +0 -95
- package/node_modules/@noble/hashes/_md.js +0 -209
- package/node_modules/@noble/hashes/_u64.d.ts +0 -36
- package/node_modules/@noble/hashes/_u64.js +0 -77
- package/node_modules/@noble/hashes/argon2.d.ts +0 -148
- package/node_modules/@noble/hashes/argon2.js +0 -577
- package/node_modules/@noble/hashes/blake1.d.ts +0 -162
- package/node_modules/@noble/hashes/blake1.js +0 -630
- package/node_modules/@noble/hashes/blake2.d.ts +0 -203
- package/node_modules/@noble/hashes/blake2.js +0 -573
- package/node_modules/@noble/hashes/blake3.d.ts +0 -68
- package/node_modules/@noble/hashes/blake3.js +0 -310
- package/node_modules/@noble/hashes/eskdf.d.ts +0 -91
- package/node_modules/@noble/hashes/eskdf.js +0 -225
- package/node_modules/@noble/hashes/hkdf.d.ts +0 -70
- package/node_modules/@noble/hashes/hkdf.js +0 -152
- package/node_modules/@noble/hashes/hmac.d.ts +0 -50
- package/node_modules/@noble/hashes/hmac.js +0 -93
- package/node_modules/@noble/hashes/index.d.ts +0 -1
- package/node_modules/@noble/hashes/index.js +0 -34
- package/node_modules/@noble/hashes/legacy.d.ts +0 -107
- package/node_modules/@noble/hashes/legacy.js +0 -348
- package/node_modules/@noble/hashes/package.json +0 -91
- package/node_modules/@noble/hashes/pbkdf2.d.ts +0 -66
- package/node_modules/@noble/hashes/pbkdf2.js +0 -176
- package/node_modules/@noble/hashes/scrypt.d.ts +0 -98
- package/node_modules/@noble/hashes/scrypt.js +0 -304
- package/node_modules/@noble/hashes/sha2.d.ts +0 -176
- package/node_modules/@noble/hashes/sha2.js +0 -446
- package/node_modules/@noble/hashes/sha3-addons.d.ts +0 -533
- package/node_modules/@noble/hashes/sha3-addons.js +0 -973
- package/node_modules/@noble/hashes/sha3.d.ts +0 -208
- package/node_modules/@noble/hashes/sha3.js +0 -475
- package/node_modules/@noble/hashes/src/_blake.ts +0 -67
- package/node_modules/@noble/hashes/src/_md.ts +0 -227
- package/node_modules/@noble/hashes/src/_u64.ts +0 -111
- package/node_modules/@noble/hashes/src/argon2.ts +0 -693
- package/node_modules/@noble/hashes/src/blake1.ts +0 -677
- package/node_modules/@noble/hashes/src/blake2.ts +0 -648
- package/node_modules/@noble/hashes/src/blake3.ts +0 -344
- package/node_modules/@noble/hashes/src/eskdf.ts +0 -265
- package/node_modules/@noble/hashes/src/hkdf.ts +0 -161
- package/node_modules/@noble/hashes/src/hmac.ts +0 -130
- package/node_modules/@noble/hashes/src/index.ts +0 -33
- package/node_modules/@noble/hashes/src/legacy.ts +0 -361
- package/node_modules/@noble/hashes/src/pbkdf2.ts +0 -219
- package/node_modules/@noble/hashes/src/scrypt.ts +0 -375
- package/node_modules/@noble/hashes/src/sha2.ts +0 -486
- package/node_modules/@noble/hashes/src/sha3-addons.ts +0 -1157
- package/node_modules/@noble/hashes/src/sha3.ts +0 -525
- package/node_modules/@noble/hashes/src/utils.ts +0 -978
- package/node_modules/@noble/hashes/src/webcrypto.ts +0 -283
- package/node_modules/@noble/hashes/utils.d.ts +0 -560
- package/node_modules/@noble/hashes/utils.js +0 -687
- package/node_modules/@noble/hashes/webcrypto.d.ts +0 -121
- package/node_modules/@noble/hashes/webcrypto.js +0 -228
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The conduct receipt (
|
|
3
|
-
*
|
|
2
|
+
* The conduct receipt (Doors W1.2 / W1.3): the engineer's private view of one session, printed
|
|
3
|
+
* when `mnemom agent` exits and when a durable session parks. On by default since 0.17.38
|
|
4
|
+
* (MNE-9013; MNEMOM_AGENT_RECEIPT=0 opts out). It prints ONLY when the server returns tally data:
|
|
5
|
+
* an org outside the record cohort (404), a caller without the role (403), no captured lineage,
|
|
6
|
+
* or a read that never answers inside the deadline all print nothing.
|
|
4
7
|
*
|
|
5
8
|
* Conduct receipt
|
|
6
9
|
* model cost $0.42
|
|
@@ -13,14 +16,14 @@
|
|
|
13
16
|
*
|
|
14
17
|
* NEVER BLOCKS EXIT. The whole read, auth refresh included, races a hard deadline
|
|
15
18
|
* (RECEIPT_DEADLINE_MS): with the API or the record builder down, exit takes at most that much
|
|
16
|
-
* longer, and the receipt
|
|
19
|
+
* longer, and the receipt prints nothing. The caller keeps its exit code; nothing here
|
|
17
20
|
* throws.
|
|
18
21
|
*
|
|
19
22
|
* The lineage key is the gateway's (x-mnemom-lineage-key, set on every response), captured by the
|
|
20
23
|
* local base-URL proxy into a state file: the CLI never derives one itself.
|
|
21
24
|
*/
|
|
22
25
|
import { readFileSync } from "node:fs";
|
|
23
|
-
import { getRecordTotals } from "./api.js";
|
|
26
|
+
import { getRecordTotals, MnemomApiError } from "./api.js";
|
|
24
27
|
import { getWebsiteUrl } from "./config.js";
|
|
25
28
|
/** The hard ceiling on what the receipt adds to exit (W1.3: at most 2 s). */
|
|
26
29
|
export const RECEIPT_DEADLINE_MS = 1800;
|
|
@@ -101,7 +104,7 @@ export function formatMu(mmu) {
|
|
|
101
104
|
/** The receipt lines for what was read (pure). */
|
|
102
105
|
export function receiptLines(read, link, trigger = "exit") {
|
|
103
106
|
const head = trigger === "park" ? "Conduct receipt (parked)" : "Conduct receipt";
|
|
104
|
-
if (!read || read.kind
|
|
107
|
+
if (!read || read.kind !== "ok") {
|
|
105
108
|
return [head, ` record not available yet: ${link}`];
|
|
106
109
|
}
|
|
107
110
|
const lines = [head, ` model cost ${formatUsd(read.modelCostMmu)}`];
|
|
@@ -117,8 +120,9 @@ export function receiptLines(read, link, trigger = "exit") {
|
|
|
117
120
|
*/
|
|
118
121
|
export async function buildConductReceipt(state, deps, trigger = "exit") {
|
|
119
122
|
const link = state.recordUrl ?? deps.fallbackUrl;
|
|
123
|
+
// No lineage captured: nothing to read, so nothing to print.
|
|
120
124
|
if (!state.lineageKey)
|
|
121
|
-
return
|
|
125
|
+
return [];
|
|
122
126
|
const now = deps.now ?? Date.now;
|
|
123
127
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
124
128
|
const deadline = now() + (deps.deadlineMs ?? RECEIPT_DEADLINE_MS);
|
|
@@ -130,6 +134,10 @@ export async function buildConductReceipt(state, deps, trigger = "exit") {
|
|
|
130
134
|
if (left <= 0)
|
|
131
135
|
break;
|
|
132
136
|
const read = await readWithin(deps.readTotals, lineageKey, left);
|
|
137
|
+
// Not in the cohort, or no role to read it: stop at once, no exit delay. An earlier good read
|
|
138
|
+
// (the cohort or role changed mid-poll) still prints; otherwise nothing does.
|
|
139
|
+
if (read.kind === "absent")
|
|
140
|
+
break;
|
|
133
141
|
// A later failure never hides an earlier good read.
|
|
134
142
|
if (read.kind === "ok" || !last)
|
|
135
143
|
last = read;
|
|
@@ -139,6 +147,9 @@ export async function buildConductReceipt(state, deps, trigger = "exit") {
|
|
|
139
147
|
break;
|
|
140
148
|
await sleep(pollMs);
|
|
141
149
|
}
|
|
150
|
+
// Only tally data prints: every read failing or timing out stays silent.
|
|
151
|
+
if (!last || last.kind !== "ok")
|
|
152
|
+
return [];
|
|
142
153
|
return receiptLines(last, link, trigger);
|
|
143
154
|
}
|
|
144
155
|
async function readWithin(readTotals, lineageKey, ms) {
|
|
@@ -171,7 +182,18 @@ export async function conductReceiptFor(launch, trigger = "exit", deps = {}) {
|
|
|
171
182
|
return buildConductReceipt(readRecordState(launch.statePath), {
|
|
172
183
|
...deps,
|
|
173
184
|
readTotals: deps.readTotals ??
|
|
174
|
-
(async (lineageKey, signal) =>
|
|
185
|
+
(async (lineageKey, signal) => {
|
|
186
|
+
try {
|
|
187
|
+
return parseTotals(await getRecordTotals(launch.orgId, lineageKey, signal));
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
// effectiveStatus, not status: the enforce hook may rewrite a 404 to a synthetic 500.
|
|
191
|
+
if (err instanceof MnemomApiError && [403, 404].includes(err.effectiveStatus)) {
|
|
192
|
+
return { kind: "absent" };
|
|
193
|
+
}
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}),
|
|
175
197
|
fallbackUrl: deps.fallbackUrl ?? `${getWebsiteUrl()}/agents/${encodeURIComponent(launch.agentId)}`,
|
|
176
198
|
}, trigger);
|
|
177
199
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Everything here is pure or dependency-injected; index.ts and the command
|
|
13
13
|
* modules are the thin drivers.
|
|
14
14
|
*/
|
|
15
|
-
export type ExperimentalId = "context-handoff" | "slack" | "implicit" | "measured-savings" | "
|
|
15
|
+
export type ExperimentalId = "context-handoff" | "slack" | "implicit" | "measured-savings" | "fuel" | "durable" | "dispatch";
|
|
16
16
|
/**
|
|
17
17
|
* Features that used to be experimental and are now on by default for everyone. Their ids are
|
|
18
18
|
* no longer in the registry, but `mnemom experimental enable|disable <id>` still accepts them
|
package/dist/lib/experimental.js
CHANGED
|
@@ -24,6 +24,7 @@ export const GRADUATED_FEATURES = {
|
|
|
24
24
|
since: "0.17.23",
|
|
25
25
|
optOut: "--no-compaction-intercept, `compaction_intercept = false` in launcher.toml, or MNEMOM_AGENT_COMPACTION_INTERCEPT=0",
|
|
26
26
|
},
|
|
27
|
+
record: { since: "0.17.38", optOut: "MNEMOM_AGENT_RECEIPT=0" },
|
|
27
28
|
};
|
|
28
29
|
/** The one-line note for `mnemom experimental enable|disable <graduated id>`; null otherwise. */
|
|
29
30
|
export function graduatedNote(id) {
|
|
@@ -85,18 +86,6 @@ export const EXPERIMENTAL_FEATURES = [
|
|
|
85
86
|
env: [],
|
|
86
87
|
},
|
|
87
88
|
},
|
|
88
|
-
{
|
|
89
|
-
// No flag, launcher.toml key or env of its own: `mnemom experimental enable` is the switch.
|
|
90
|
-
id: "record",
|
|
91
|
-
summary: "Print the conduct receipt at exit: the session's measured model cost and μ, or the record link",
|
|
92
|
-
since: "0.17.32",
|
|
93
|
-
gates: {
|
|
94
|
-
flags: [],
|
|
95
|
-
configKeys: [],
|
|
96
|
-
commands: [],
|
|
97
|
-
env: [],
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
89
|
{
|
|
101
90
|
id: "fuel",
|
|
102
91
|
summary: "The fuel step: `mnemom agent fuel <file>` seals a batch of job contracts with one confirmation and commits them to the repo",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Nothing here decides an outcome: each call returns its status, body and error code, and the job
|
|
9
9
|
* (job.ts) maps them to 34.12's reasons. Never throws: a network failure is status 0.
|
|
10
10
|
*/
|
|
11
|
+
import type { RecordCommitBody } from "./record-commit.js";
|
|
11
12
|
import type { ExecAppendBody } from "./exec-append.js";
|
|
12
13
|
export interface HttpResult {
|
|
13
14
|
readonly status: number;
|
|
@@ -34,6 +35,8 @@ export declare const JOB_READ_PATH: (sessionId: string) => string;
|
|
|
34
35
|
/** The served bundle fetch, per claim (W-L12). */
|
|
35
36
|
export declare const BUNDLE_FETCH_PATH: (sessionId: string) => string;
|
|
36
37
|
export declare const JWKS_PATH = "/v1/.well-known/jwks.json";
|
|
38
|
+
/** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
|
|
39
|
+
export declare const RECORD_COMMITS_PATH: (orgId: string) => string;
|
|
37
40
|
export interface RunnerApi {
|
|
38
41
|
readyList(limit: number): Promise<HttpResult>;
|
|
39
42
|
attest(sessionId: string, nonce: string): Promise<HttpResult>;
|
|
@@ -63,6 +66,8 @@ export interface RunnerApi {
|
|
|
63
66
|
job(sessionId: string): Promise<HttpResult>;
|
|
64
67
|
bundle(sessionId: string, leaseKey: string): Promise<HttpResult>;
|
|
65
68
|
jwks(): Promise<HttpResult>;
|
|
69
|
+
/** One RecordCommit/v1 body, with the lease key; the api pins it to the session's lineage. */
|
|
70
|
+
recordCommit(orgId: string, body: RecordCommitBody, leaseKey: string): Promise<HttpResult>;
|
|
66
71
|
}
|
|
67
72
|
export declare function runnerApi(opts: {
|
|
68
73
|
apiUrl: string;
|
|
@@ -34,6 +34,9 @@ export const JOB_READ_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/
|
|
|
34
34
|
/** The served bundle fetch, per claim (W-L12). */
|
|
35
35
|
export const BUNDLE_FETCH_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/bundle`;
|
|
36
36
|
export const JWKS_PATH = "/v1/.well-known/jwks.json";
|
|
37
|
+
/** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
|
|
38
|
+
export const RECORD_COMMITS_PATH = (orgId) => `/v1/orgs/${encodeURIComponent(orgId)}/record/commits`;
|
|
39
|
+
const ORG_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
37
40
|
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
38
41
|
export function runnerApi(opts) {
|
|
39
42
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
@@ -93,5 +96,10 @@ export function runnerApi(opts) {
|
|
|
93
96
|
job: (id) => call(JOB_READ_PATH(scoped(id)), "GET", ex),
|
|
94
97
|
bundle: (id, leaseKey) => call(BUNDLE_FETCH_PATH(scoped(id)), "GET", leaseKey),
|
|
95
98
|
jwks: () => call(JWKS_PATH, "GET", null),
|
|
99
|
+
recordCommit: (orgId, body, leaseKey) => {
|
|
100
|
+
if (!ORG_ID_RE.test(orgId))
|
|
101
|
+
throw new RangeError("org id is not a Mnemom org id");
|
|
102
|
+
return call(RECORD_COMMITS_PATH(orgId), "POST", leaseKey, body);
|
|
103
|
+
},
|
|
96
104
|
};
|
|
97
105
|
}
|
package/dist/lib/runner/job.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { type HttpResult, type RunnerApi } from "./executor-client.js";
|
|
|
15
15
|
import { type OutcomeFields, type RefusedReason } from "./outcome.js";
|
|
16
16
|
import { type PhaseInvocation } from "./phases.js";
|
|
17
17
|
import { type ModelProxy, type ProxyState } from "./proxy.js";
|
|
18
|
+
import type { RecordCommitBody } from "./record-commit.js";
|
|
18
19
|
import { type RunnerFailMode } from "./settings.js";
|
|
19
20
|
/** A claimed job's working copy. */
|
|
20
21
|
export interface Workspace {
|
|
@@ -45,6 +46,13 @@ export interface JobDeps {
|
|
|
45
46
|
openPr(repo: string, ws: Workspace, title: string, body: string): Promise<string>;
|
|
46
47
|
/** Commits on the job branch that the base does not have; 0 means there is nothing to open. */
|
|
47
48
|
commitsAhead(ws: Workspace): Promise<number>;
|
|
49
|
+
/** The working copy's HEAD sha before a phase runs; null only for a clone with no commit. Throws otherwise. */
|
|
50
|
+
headSha(ws: Workspace): Promise<string | null>;
|
|
51
|
+
/**
|
|
52
|
+
* RecordCommit/v1 bodies for the commits made since `since` (null: since the base), oldest
|
|
53
|
+
* first, under `lineageKey` and the job's repo. A commit that cannot be read is left out.
|
|
54
|
+
*/
|
|
55
|
+
commitBodies(ws: Workspace, since: string | null, lineageKey: string, repo: string): Promise<RecordCommitBody[]>;
|
|
48
56
|
enableAutoMerge(prUrl: string): Promise<void>;
|
|
49
57
|
cleanup(ws: Workspace): Promise<void>;
|
|
50
58
|
}
|
|
@@ -69,6 +77,35 @@ export declare function authRefusal(r: HttpResult): RefusedReason | null;
|
|
|
69
77
|
* outcome. Throws only on a runner bug.
|
|
70
78
|
*/
|
|
71
79
|
export declare function runJob(sessionId: string, opts: JobOptions, deps: JobDeps): Promise<JobResult>;
|
|
80
|
+
/** What the job reported to Record's commit index; logged once, never part of the outcome. */
|
|
81
|
+
export interface CommitReport {
|
|
82
|
+
reported: number;
|
|
83
|
+
/** Refused by the api (4xx other than 404) or unreachable: left out of the index. */
|
|
84
|
+
dropped: number;
|
|
85
|
+
/** Made in a phase whose model calls left no lineage key, so there was nothing to file under. */
|
|
86
|
+
unlinked: number;
|
|
87
|
+
/** Phases whose starting HEAD could not be read: their commits are never guessed at. */
|
|
88
|
+
skipped: number;
|
|
89
|
+
/** The index answered 404 (Record dark for the org): nothing more is sent for this job. */
|
|
90
|
+
dark: boolean;
|
|
91
|
+
}
|
|
92
|
+
/** Waits before each retry of a 429, 5xx or unreachable report; then the commit is dropped. */
|
|
93
|
+
export declare const COMMIT_RETRY_MS: readonly [1000, 3000];
|
|
94
|
+
/**
|
|
95
|
+
* Report the commits one phase made (ruling 34.05 (a)): each under the lineage key the gateway
|
|
96
|
+
* answered for that phase's conversation, with the lease key, from this process. Never throws and
|
|
97
|
+
* never changes the outcome; the lease key is sent only as the request credential, never logged.
|
|
98
|
+
*/
|
|
99
|
+
export declare function reportPhaseCommits(deps: JobDeps, report: CommitReport, p: {
|
|
100
|
+
ws: Workspace;
|
|
101
|
+
/** The HEAD before the phase; null: a clone with no commit; undefined: it could not be read. */
|
|
102
|
+
before: string | null | undefined;
|
|
103
|
+
conversationId: string;
|
|
104
|
+
proxy: Pick<ModelProxy, "lineageFor">;
|
|
105
|
+
orgId: string;
|
|
106
|
+
repo: string;
|
|
107
|
+
leaseKey: string;
|
|
108
|
+
}): Promise<void>;
|
|
72
109
|
/**
|
|
73
110
|
* Report a failed job after claim (34.12 (6), (10)); a refusal releases the lease instead. The
|
|
74
111
|
* fail reason vocabulary is the Durable lead's to rule (A3 accepts only transcript_lost today); a
|
package/dist/lib/runner/job.js
CHANGED
|
@@ -20,7 +20,7 @@ import { errorCode, errorDetails } from "./executor-client.js";
|
|
|
20
20
|
import { doneOutcome, failedOutcome, phaseReason, refusedOutcome, } from "./outcome.js";
|
|
21
21
|
import { planPhase } from "./phases.js";
|
|
22
22
|
import { canonNodeKey, prBody } from "./pr-body.js";
|
|
23
|
-
import { requestSummary } from "./proxy.js";
|
|
23
|
+
import { phaseConversationId, requestSummary } from "./proxy.js";
|
|
24
24
|
import { resolveMergeStrategy } from "./settings.js";
|
|
25
25
|
const TOLERATED = RUNNER_OPEN_MODE_TOLERATED;
|
|
26
26
|
const OPEN_MODE_RETRIES = 3;
|
|
@@ -189,6 +189,13 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
189
189
|
let leaseLost = false;
|
|
190
190
|
let ws = null;
|
|
191
191
|
let proxy = null;
|
|
192
|
+
const commits = {
|
|
193
|
+
reported: 0,
|
|
194
|
+
dropped: 0,
|
|
195
|
+
unlinked: 0,
|
|
196
|
+
skipped: 0,
|
|
197
|
+
dark: false,
|
|
198
|
+
};
|
|
192
199
|
let heartbeat = null;
|
|
193
200
|
const phaseAbort = new AbortController();
|
|
194
201
|
const onStop = () => phaseAbort.abort();
|
|
@@ -303,10 +310,17 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
303
310
|
contractValue: header.value,
|
|
304
311
|
leaseKey: claim.leaseKey,
|
|
305
312
|
agentId: job.agent_id,
|
|
313
|
+
conversationId: phaseConversationId(sessionId, claim.epoch, phases[0].order),
|
|
306
314
|
};
|
|
307
315
|
proxy = await deps.startProxy(state);
|
|
308
316
|
let opensPr = false;
|
|
309
317
|
for (const phase of phases) {
|
|
318
|
+
state.conversationId = phaseConversationId(sessionId, claim.epoch, phase.order);
|
|
319
|
+
// undefined: the HEAD could not be read, so this phase's commits cannot be told apart.
|
|
320
|
+
const before = await deps.headSha(ws).catch((e) => {
|
|
321
|
+
deps.log(`reading HEAD before phase ${phase.order} failed: ${e.message}`);
|
|
322
|
+
return undefined;
|
|
323
|
+
});
|
|
310
324
|
const planned = planPhase(phase);
|
|
311
325
|
if (!planned.ok) {
|
|
312
326
|
deps.log(`phase ${phase.order} ${phase.name}: ${planned.detail}`);
|
|
@@ -354,6 +368,15 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
354
368
|
}
|
|
355
369
|
break;
|
|
356
370
|
}
|
|
371
|
+
await reportPhaseCommits(deps, commits, {
|
|
372
|
+
ws,
|
|
373
|
+
before,
|
|
374
|
+
conversationId: state.conversationId,
|
|
375
|
+
proxy,
|
|
376
|
+
orgId: bundle.org_id,
|
|
377
|
+
repo: job.repo,
|
|
378
|
+
leaseKey: claim.leaseKey,
|
|
379
|
+
});
|
|
357
380
|
}
|
|
358
381
|
if (opensPr) {
|
|
359
382
|
try {
|
|
@@ -390,12 +413,62 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
390
413
|
await proxy.close();
|
|
391
414
|
// One line per job that reached its phases: what every model request carried (W3.11).
|
|
392
415
|
deps.log(`model requests: ${JSON.stringify(requestSummary(proxy.forwarded))}`);
|
|
416
|
+
deps.log(`commits reported: ${JSON.stringify(commits)}`);
|
|
393
417
|
}
|
|
394
418
|
// A workspace left behind never changes the outcome; it is logged for the operator.
|
|
395
419
|
if (ws)
|
|
396
420
|
await deps.cleanup(ws).catch((e) => deps.log(`cleanup failed: ${e.message}`));
|
|
397
421
|
}
|
|
398
422
|
}
|
|
423
|
+
/** Waits before each retry of a 429, 5xx or unreachable report; then the commit is dropped. */
|
|
424
|
+
export const COMMIT_RETRY_MS = [1_000, 3_000];
|
|
425
|
+
/**
|
|
426
|
+
* Report the commits one phase made (ruling 34.05 (a)): each under the lineage key the gateway
|
|
427
|
+
* answered for that phase's conversation, with the lease key, from this process. Never throws and
|
|
428
|
+
* never changes the outcome; the lease key is sent only as the request credential, never logged.
|
|
429
|
+
*/
|
|
430
|
+
export async function reportPhaseCommits(deps, report, p) {
|
|
431
|
+
if (report.dark)
|
|
432
|
+
return;
|
|
433
|
+
if (p.before === undefined) {
|
|
434
|
+
report.skipped += 1;
|
|
435
|
+
deps.log(`${p.conversationId}: commits not reported, the phase's starting HEAD is unknown`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const lineageKey = p.proxy.lineageFor(p.conversationId);
|
|
440
|
+
if (lineageKey === null) {
|
|
441
|
+
const n = (await deps.commitBodies(p.ws, p.before, "-", p.repo)).length;
|
|
442
|
+
report.unlinked += n;
|
|
443
|
+
if (n > 0)
|
|
444
|
+
deps.log(`${n} commit(s) in ${p.conversationId} have no lineage key: not reported`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
for (const body of await deps.commitBodies(p.ws, p.before, lineageKey, p.repo)) {
|
|
448
|
+
let res = await deps.api.recordCommit(p.orgId, body, p.leaseKey);
|
|
449
|
+
for (const wait of COMMIT_RETRY_MS) {
|
|
450
|
+
if (!(res.status === 0 || res.status === 429 || res.status >= 500))
|
|
451
|
+
break;
|
|
452
|
+
await deps.sleep(wait);
|
|
453
|
+
res = await deps.api.recordCommit(p.orgId, body, p.leaseKey);
|
|
454
|
+
}
|
|
455
|
+
if (res.status >= 200 && res.status < 300)
|
|
456
|
+
report.reported += 1;
|
|
457
|
+
else if (res.status === 404) {
|
|
458
|
+
report.dark = true;
|
|
459
|
+
deps.log("the commit index answered 404 (Record is off for this org): commits not reported");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
report.dropped += 1;
|
|
464
|
+
deps.log(`commit ${body.sha.slice(0, 12)} not reported: ${res.code ?? res.networkError ?? res.status}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
deps.log(`commit report failed: ${e.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
399
472
|
async function fetchBundle(deps, sessionId, claim) {
|
|
400
473
|
const [jwksRes, res] = await Promise.all([
|
|
401
474
|
deps.api.jwks(),
|
|
@@ -15,8 +15,13 @@ export declare const CAPABILITY_TOOLS: Readonly<Record<string, {
|
|
|
15
15
|
tools: string[];
|
|
16
16
|
allowed: string[];
|
|
17
17
|
}>>;
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Env names a phase may inherit: a working PATH, the shell and locale. Nothing else. Claude Code's
|
|
20
|
+
* Bash tool needs SHELL to name a POSIX shell; without it, on an image with no bash on PATH (Alpine),
|
|
21
|
+
* every Bash call fails and a phase can write files but never commit them (door2-path image run
|
|
22
|
+
* 41940973594 ended pr_no_changes).
|
|
23
|
+
*/
|
|
24
|
+
export declare const PHASE_ENV_PASSTHROUGH: readonly ["PATH", "SHELL", "LANG", "TZ"];
|
|
20
25
|
/** Not a secret: lets claude start; the proxy drops it before the gateway (ruling: option (i)). */
|
|
21
26
|
export declare const PHASE_MODEL_KEY_PLACEHOLDER = "mnemom-runner-placeholder";
|
|
22
27
|
export type ToolGrant = {
|
|
@@ -21,8 +21,13 @@ export const CAPABILITY_TOOLS = Object.freeze({
|
|
|
21
21
|
// The runner opens the PR itself once every phase has passed; the model gets no tool for it.
|
|
22
22
|
"pr.open": { tools: [], allowed: [] },
|
|
23
23
|
});
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Env names a phase may inherit: a working PATH, the shell and locale. Nothing else. Claude Code's
|
|
26
|
+
* Bash tool needs SHELL to name a POSIX shell; without it, on an image with no bash on PATH (Alpine),
|
|
27
|
+
* every Bash call fails and a phase can write files but never commit them (door2-path image run
|
|
28
|
+
* 41940973594 ended pr_no_changes).
|
|
29
|
+
*/
|
|
30
|
+
export const PHASE_ENV_PASSTHROUGH = ["PATH", "SHELL", "LANG", "TZ"];
|
|
26
31
|
/** Not a secret: lets claude start; the proxy drops it before the gateway (ruling: option (i)). */
|
|
27
32
|
export const PHASE_MODEL_KEY_PLACEHOLDER = "mnemom-runner-placeholder";
|
|
28
33
|
export function toolsFor(capabilityIds) {
|
|
@@ -7,24 +7,39 @@
|
|
|
7
7
|
* Anthropic key in x-api-key (ruling 34.30.1; the phase only ever holds a placeholder), the job read's agent id in
|
|
8
8
|
* x-mnemom-agent-id (the gateway checks it against the lease's session), and the job's
|
|
9
9
|
* sealed contract goes in `x-mnemom-contract` (exactly one, the sealed version). The phase never
|
|
10
|
-
* sees the lease key.
|
|
10
|
+
* sees the lease key. Each phase gets its own `x-mnemom-conversation-id` (from the session, lease
|
|
11
|
+
* epoch and phase order), so the gateway keys one lineage per phase instead of a body-hash one per
|
|
12
|
+
* request, and the `x-mnemom-lineage-key` it answers is kept for the phase's commit report. A 409 `contract_mismatch` from the gateway (34.11) is reported to the job,
|
|
11
13
|
* which refetches and continues only on a higher sealed version, else fails closed.
|
|
12
14
|
*/
|
|
13
15
|
import { request as httpRequest, type IncomingMessage, type ServerResponse } from "node:http";
|
|
14
16
|
export declare const LEASE_KEY_HEADER = "x-mnemom-api-key";
|
|
15
17
|
export declare const AGENT_ID_HEADER = "x-mnemom-agent-id";
|
|
18
|
+
export declare const CONVERSATION_ID_HEADER = "x-mnemom-conversation-id";
|
|
19
|
+
export declare const LINEAGE_KEY_HEADER = "x-mnemom-lineage-key";
|
|
20
|
+
/**
|
|
21
|
+
* One phase's conversation id: the session, the lease epoch and the phase order, so a retried
|
|
22
|
+
* claim or another phase never shares a lineage. Within the gateway's 1-256 printable, no-`#` id.
|
|
23
|
+
*/
|
|
24
|
+
export declare function phaseConversationId(sessionId: string, epoch: number, order: number): string;
|
|
16
25
|
export interface ProxyState {
|
|
17
26
|
/** The sealed header value to send; swapped in place after an accepted amend. */
|
|
18
27
|
contractValue: string;
|
|
19
28
|
leaseKey: string;
|
|
20
29
|
/** The session's agent, from the job read; always sent, a phase's own value never is. */
|
|
21
30
|
agentId: string;
|
|
31
|
+
/** The running phase's conversation id; the job sets it before each phase. */
|
|
32
|
+
conversationId: string;
|
|
22
33
|
}
|
|
23
34
|
export interface ForwardedRequest {
|
|
24
35
|
method: string;
|
|
25
36
|
path: string;
|
|
26
37
|
contract: string;
|
|
27
38
|
status: number;
|
|
39
|
+
/** The conversation id the request carried. */
|
|
40
|
+
conversation: string;
|
|
41
|
+
/** The gateway's `x-mnemom-lineage-key` answer, when it sent a well-formed one. */
|
|
42
|
+
lineage: string | null;
|
|
28
43
|
}
|
|
29
44
|
export interface ModelProxy {
|
|
30
45
|
url: string;
|
|
@@ -34,17 +49,26 @@ export interface ModelProxy {
|
|
|
34
49
|
contractMismatch: {
|
|
35
50
|
expected_version: number | null;
|
|
36
51
|
} | null;
|
|
52
|
+
/** The lineage key the gateway last answered for a conversation id, or null when none came. */
|
|
53
|
+
lineageFor(conversationId: string): string | null;
|
|
37
54
|
close(): Promise<void>;
|
|
38
55
|
}
|
|
39
56
|
/** Outbound headers for one request: the phase's own, minus anything the proxy decides. */
|
|
40
57
|
export declare function outboundHeaders(incoming: IncomingMessage["headers"], state: ProxyState, modelKey: string): Record<string, string | string[] | undefined>;
|
|
41
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* What the job's model requests carried: each contract by its sha256, in first-seen order, and the
|
|
60
|
+
* lineage key the gateway answered for each conversation (one per phase), in first-seen order.
|
|
61
|
+
*/
|
|
42
62
|
export interface RequestSummary {
|
|
43
63
|
count: number;
|
|
44
64
|
contracts: {
|
|
45
65
|
contract_sha256: string;
|
|
46
66
|
requests: number;
|
|
47
67
|
}[];
|
|
68
|
+
lineages: {
|
|
69
|
+
conversation_id: string;
|
|
70
|
+
lineage_key: string | null;
|
|
71
|
+
}[];
|
|
48
72
|
}
|
|
49
73
|
/**
|
|
50
74
|
* One job's forwarded requests, summarised for the operator's log and for the door2-path header
|
package/dist/lib/runner/proxy.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* Anthropic key in x-api-key (ruling 34.30.1; the phase only ever holds a placeholder), the job read's agent id in
|
|
8
8
|
* x-mnemom-agent-id (the gateway checks it against the lease's session), and the job's
|
|
9
9
|
* sealed contract goes in `x-mnemom-contract` (exactly one, the sealed version). The phase never
|
|
10
|
-
* sees the lease key.
|
|
10
|
+
* sees the lease key. Each phase gets its own `x-mnemom-conversation-id` (from the session, lease
|
|
11
|
+
* epoch and phase order), so the gateway keys one lineage per phase instead of a body-hash one per
|
|
12
|
+
* request, and the `x-mnemom-lineage-key` it answers is kept for the phase's commit report. A 409 `contract_mismatch` from the gateway (34.11) is reported to the job,
|
|
11
13
|
* which refetches and continues only on a higher sealed version, else fails closed.
|
|
12
14
|
*/
|
|
13
15
|
import { createServer, request as httpRequest, } from "node:http";
|
|
@@ -16,6 +18,17 @@ import { createHash } from "node:crypto";
|
|
|
16
18
|
import { applySealedContract, CONTRACT_HEADER } from "./contract-header.js";
|
|
17
19
|
export const LEASE_KEY_HEADER = "x-mnemom-api-key";
|
|
18
20
|
export const AGENT_ID_HEADER = "x-mnemom-agent-id";
|
|
21
|
+
export const CONVERSATION_ID_HEADER = "x-mnemom-conversation-id";
|
|
22
|
+
export const LINEAGE_KEY_HEADER = "x-mnemom-lineage-key";
|
|
23
|
+
/** The gateway's lineage key as echoed: short, printable, no spaces. Anything else is not kept. */
|
|
24
|
+
const LINEAGE_KEY_RE = /^[\x21-\x7e]{1,256}$/;
|
|
25
|
+
/**
|
|
26
|
+
* One phase's conversation id: the session, the lease epoch and the phase order, so a retried
|
|
27
|
+
* claim or another phase never shares a lineage. Within the gateway's 1-256 printable, no-`#` id.
|
|
28
|
+
*/
|
|
29
|
+
export function phaseConversationId(sessionId, epoch, order) {
|
|
30
|
+
return `durable-${sessionId}-e${epoch}-p${order}`;
|
|
31
|
+
}
|
|
19
32
|
/** Hop-by-hop headers (RFC 9110 7.6.1) and the ones the proxy owns. */
|
|
20
33
|
const DROP = new Set([
|
|
21
34
|
"connection",
|
|
@@ -43,8 +56,16 @@ export function outboundHeaders(incoming, state, modelKey) {
|
|
|
43
56
|
kept[LEASE_KEY_HEADER] = state.leaseKey;
|
|
44
57
|
kept["x-api-key"] = modelKey;
|
|
45
58
|
kept[AGENT_ID_HEADER] = state.agentId;
|
|
59
|
+
kept[CONVERSATION_ID_HEADER] = state.conversationId;
|
|
46
60
|
return applySealedContract(kept, state.contractValue);
|
|
47
61
|
}
|
|
62
|
+
/** The last well-formed lineage key answered for each conversation, in first-seen order. */
|
|
63
|
+
function lineagesOf(forwarded) {
|
|
64
|
+
const m = new Map();
|
|
65
|
+
for (const f of forwarded)
|
|
66
|
+
m.set(f.conversation, f.lineage ?? m.get(f.conversation) ?? null);
|
|
67
|
+
return m;
|
|
68
|
+
}
|
|
48
69
|
/**
|
|
49
70
|
* One job's forwarded requests, summarised for the operator's log and for the door2-path header
|
|
50
71
|
* check (W3.11): the sha256 of each header value's decoded bytes is the contract it sealed to.
|
|
@@ -58,6 +79,10 @@ export function requestSummary(forwarded) {
|
|
|
58
79
|
return {
|
|
59
80
|
count: forwarded.length,
|
|
60
81
|
contracts: [...by].map(([contract_sha256, requests]) => ({ contract_sha256, requests })),
|
|
82
|
+
lineages: [...lineagesOf(forwarded)].map(([conversation_id, lineage_key]) => ({
|
|
83
|
+
conversation_id,
|
|
84
|
+
lineage_key,
|
|
85
|
+
})),
|
|
61
86
|
};
|
|
62
87
|
}
|
|
63
88
|
/** The largest 409 body the proxy reads for a contract mismatch. */
|
|
@@ -102,11 +127,13 @@ export async function startModelProxy(gatewayUrl, state, modelKey) {
|
|
|
102
127
|
url: "",
|
|
103
128
|
forwarded,
|
|
104
129
|
contractMismatch: null,
|
|
130
|
+
lineageFor: (id) => lineagesOf(forwarded).get(id) ?? null,
|
|
105
131
|
close: async () => { },
|
|
106
132
|
};
|
|
107
133
|
const server = createServer((req, res) => {
|
|
108
134
|
const headers = outboundHeaders(req.headers, state, modelKey);
|
|
109
135
|
const sent = headers[CONTRACT_HEADER];
|
|
136
|
+
const conversation = state.conversationId;
|
|
110
137
|
const out = send({
|
|
111
138
|
protocol: upstream.protocol,
|
|
112
139
|
hostname: upstream.hostname,
|
|
@@ -116,11 +143,14 @@ export async function startModelProxy(gatewayUrl, state, modelKey) {
|
|
|
116
143
|
headers,
|
|
117
144
|
}, (up) => {
|
|
118
145
|
const status = up.statusCode ?? 502;
|
|
146
|
+
const echoed = up.headers[LINEAGE_KEY_HEADER];
|
|
119
147
|
forwarded.push({
|
|
120
148
|
method: req.method ?? "GET",
|
|
121
149
|
path: req.url ?? "/",
|
|
122
150
|
contract: sent,
|
|
123
151
|
status,
|
|
152
|
+
conversation,
|
|
153
|
+
lineage: typeof echoed === "string" && LINEAGE_KEY_RE.test(echoed) ? echoed : null,
|
|
124
154
|
});
|
|
125
155
|
if (status === 409) {
|
|
126
156
|
// Buffered to read the mismatch, up to a cap; a larger body is relayed unread.
|
|
@@ -162,6 +192,8 @@ export async function startModelProxy(gatewayUrl, state, modelKey) {
|
|
|
162
192
|
path: req.url ?? "/",
|
|
163
193
|
contract: sent,
|
|
164
194
|
status: 502,
|
|
195
|
+
conversation,
|
|
196
|
+
lineage: null,
|
|
165
197
|
});
|
|
166
198
|
upstreamFailed(res, e);
|
|
167
199
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RecordCommit/v1 body Record's commit index takes (`POST /v1/orgs/{org}/record/commits`).
|
|
3
|
+
* It lives here so the runner can type its commit report without loading the CLI's commit reporter
|
|
4
|
+
* (which spawns git and reads auth); lib/commit-report.ts re-exports both names.
|
|
5
|
+
*/
|
|
6
|
+
/** The commit index body's schema tag (shared with the fleet worker's reporter). */
|
|
7
|
+
export declare const COMMIT_SCHEMA = "RecordCommit/v1";
|
|
8
|
+
/** RecordCommit/v1: strings only, every field required; the route rejects an unknown field. */
|
|
9
|
+
export interface RecordCommitBody {
|
|
10
|
+
schema: typeof COMMIT_SCHEMA;
|
|
11
|
+
lineage_key: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
branch: string;
|
|
14
|
+
sha: string;
|
|
15
|
+
patch_id: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RecordCommit/v1 body Record's commit index takes (`POST /v1/orgs/{org}/record/commits`).
|
|
3
|
+
* It lives here so the runner can type its commit report without loading the CLI's commit reporter
|
|
4
|
+
* (which spawns git and reads auth); lib/commit-report.ts re-exports both names.
|
|
5
|
+
*/
|
|
6
|
+
/** The commit index body's schema tag (shared with the fleet worker's reporter). */
|
|
7
|
+
export const COMMIT_SCHEMA = "RecordCommit/v1";
|
|
@@ -56,6 +56,18 @@ export interface EmailReport {
|
|
|
56
56
|
*/
|
|
57
57
|
cost_per_merged_pr?: CostPerPr["cost_per_merged_pr"];
|
|
58
58
|
cost_by_pr_size?: CostPerPr["cost_by_pr_size"];
|
|
59
|
+
/**
|
|
60
|
+
* The per-repo counts, by name (MNE-9634). Always sent, at every share level: the emailed report is the full
|
|
61
|
+
* report. Optional on the wire so an API that predates it can be retried without it. Capped at 500 repos
|
|
62
|
+
* (busiest first), valid names only, each name once.
|
|
63
|
+
*/
|
|
64
|
+
by_repo?: Array<{
|
|
65
|
+
repo: string;
|
|
66
|
+
pr_count: number;
|
|
67
|
+
revert_count: number;
|
|
68
|
+
automatable_count: number;
|
|
69
|
+
examined_count: number;
|
|
70
|
+
}>;
|
|
59
71
|
}
|
|
60
72
|
export interface ReportEmailBody {
|
|
61
73
|
report: EmailReport;
|
|
@@ -63,6 +75,9 @@ export interface ReportEmailBody {
|
|
|
63
75
|
coverage: {
|
|
64
76
|
complete: boolean;
|
|
65
77
|
};
|
|
78
|
+
/** The extra recipients the runner added. Absent when none. */
|
|
79
|
+
emails?: string[];
|
|
80
|
+
/** Only set on the retry to an API that predates `emails`: it reads one extra address under this name. */
|
|
66
81
|
cto_email?: string;
|
|
67
82
|
}
|
|
68
83
|
/**
|
|
@@ -84,9 +99,9 @@ export declare function buildEmailReport(result: ScanResult, o: {
|
|
|
84
99
|
/** The one spend source the cost figures are split from; null/absent leaves them out. */
|
|
85
100
|
costSpend?: CostSpend | null;
|
|
86
101
|
}): EmailReport;
|
|
87
|
-
export declare function buildEmailBody(report: EmailReport, level: ShareLevel, complete: boolean,
|
|
102
|
+
export declare function buildEmailBody(report: EmailReport, level: ShareLevel, complete: boolean, extraEmails?: string[]): ReportEmailBody;
|
|
88
103
|
/** Closed set of failure names; each has fixed text. */
|
|
89
|
-
export type EmailFailure = "presend_check_failed" | "email_not_sent" | "not_signed_in" | "not_a_member" | "report_rejected" | "no_recipients" | "runner_not_emailed" | "network_error" | "unexpected_response";
|
|
104
|
+
export type EmailFailure = "presend_check_failed" | "email_not_sent" | "not_signed_in" | "not_a_member" | "report_rejected" | "no_recipients" | "runner_not_emailed" | "emails_rejected" | "email_domain_not_allowed" | "admins_not_found" | "network_error" | "unexpected_response";
|
|
90
105
|
export declare const failureMessage: (f: EmailFailure) => string;
|
|
91
106
|
export declare const DRY_RUN_MSG = "Report email is in dry-run on this environment; nothing was sent.";
|
|
92
107
|
export declare const UNAVAILABLE_MSG = "Report email is not available for this org yet; nothing was sent.";
|
|
@@ -97,6 +112,8 @@ export declare class ReportEmailError extends Error {
|
|
|
97
112
|
export type EmailOutcome = {
|
|
98
113
|
status: "sent";
|
|
99
114
|
recipients: number;
|
|
115
|
+
adminsMissed?: number;
|
|
116
|
+
limited?: true;
|
|
100
117
|
} | {
|
|
101
118
|
status: "dry_run";
|
|
102
119
|
} | {
|