Public: anyone can read this; the crew can change it.
Sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:13750cmn0i is past its budget: trevato/forge: "MCP backups > a listing that fails is a short sentence with no key in it" failed on 6 pulls in the last… #8146
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/8113-sentinel-shared-01m23vygyffx5hg2f5vm8r74 → main 2 commits fork
4 files
changed +9
−5
src/http/mcp/tools/backups.ts +1 −1
| @@ -14,7 +14,7 @@ export const tool: Tool = { | ||
| 14 | 14 | adminOnly: true, |
| 15 | 15 | async run(ctx, user) { |
| 16 | 16 | if (!user.isAdmin) return bad("not_found", "not found"); |
| 17 | const dest = resolveDestination({ db: ctx.app.db, dataDir: ctx.app.config.dataDir, bus: ctx.app.bus }); | |
| 17 | const dest = resolveDestination({ db: ctx.app.db, dataDir: ctx.app.config.dataDir, bus: ctx.app.bus, attempts: 1 }); | |
| 18 | 18 | const svc = snapshotsOf(ctx.app); |
| 19 | 19 | const state = svc?.mirrored(); |
| 20 | 20 | const need = mirrorNeed(state, Date.now()); |
src/snapshot/destination.ts +4 −2
| @@ -43,6 +43,8 @@ export interface Sources { | ||
| 43 | 43 | readonly dataDir?: string | undefined; |
| 44 | 44 | readonly bus?: EventBus | undefined; |
| 45 | 45 | readonly env?: Record<string, string | undefined>; |
| 46 | /** Tries per store call; a status read asks for one so a dead store answers at once, not after the backoff. */ | |
| 47 | readonly attempts?: number; | |
| 46 | 48 | } |
| 47 | 49 | |
| 48 | 50 | /** The sealed target, or undefined when the four required fields are not all set. */ |
| @@ -67,11 +69,11 @@ export function sealedTarget(s: Sources): S3Target | undefined { | ||
| 67 | 69 | export function resolveDestination(s: Sources = {}): Destination | undefined { |
| 68 | 70 | const env = s.env ?? process.env; |
| 69 | 71 | const sealed = sealedTarget(s); |
| 70 | if (sealed) return { source: "sealed", store: s3Store(sealed) }; | |
| 72 | if (sealed) return { source: "sealed", store: s3Store(sealed, s.attempts) }; | |
| 71 | 73 | const dir = env["FORGE_SNAPSHOT_DIR"]?.trim(); |
| 72 | 74 | if (dir) return { source: "dir", store: dirStore(dir) }; |
| 73 | 75 | const fromEnv = s3FromEnv(env); |
| 74 | if (fromEnv) return { source: "env", store: s3Store(fromEnv) }; | |
| 76 | if (fromEnv) return { source: "env", store: s3Store(fromEnv, s.attempts) }; | |
| 75 | 77 | return undefined; |
| 76 | 78 | } |
| 77 | 79 |
src/snapshot/store.ts +2 −2
| @@ -180,8 +180,8 @@ function answered<T>(attempt: Promise<T>, ms: number): Promise<T> { | ||
| 180 | 180 | return Promise.race([attempt, deadline]).finally(() => clearTimeout(timer)); |
| 181 | 181 | } |
| 182 | 182 | |
| 183 | export function s3Store(target: S3Target): Store { | |
| 184 | return withRetries(rawS3Store(target)); | |
| 183 | export function s3Store(target: S3Target, attempts?: number): Store { | |
| 184 | return withRetries(rawS3Store(target), attempts === undefined ? {} : { attempts }); | |
| 185 | 185 | } |
| 186 | 186 | |
| 187 | 187 | function rawS3Store(target: S3Target): Store { |
test/mcp-backups.test.ts +2
| @@ -94,6 +94,8 @@ describe("MCP backups", () => { | ||
| 94 | 94 | expect(out.snapshots).toEqual([]); |
| 95 | 95 | expect(typeof out.listing).toBe("string"); |
| 96 | 96 | expect(out.listing.length).toBeLessThanOrEqual(200); |
| 97 | // One try, not the store's five with 7.5 s of backoff between them. | |
| 98 | expect(out.listing).toContain("after 1 attempt:"); | |
| 97 | 99 | expect(out.listing).not.toContain(secret); |
| 98 | 100 | expect(out.listing).not.toContain("ACCESSKEY-do-not-leak"); |
| 99 | 101 | } finally { |
new above
offline