GIT17: the forge module deletes seen keys past 30 days on each step-ended and at its start; git_seen_expire runs it now
Builds: design 47 G §581, §585, §487 (TypeScript tests not run here: no installed dependencies) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -24,6 +24,10 @@ export const SEEN = "seen.";
|
||||
/** The template state's cap (design 47 *The runner's kept data*): the fill is shown against it. */
|
||||
export const CAP_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
/** How long a `seen` key is kept after its `step-ended`, and how many go on one event (§581). */
|
||||
export const SEEN_DAYS = 30;
|
||||
export const SEEN_MOST_PER_EVENT = 100;
|
||||
|
||||
/** What a `seen` key holds: a commit's fingerprints at one module. */
|
||||
export interface Seen {
|
||||
v: 1;
|
||||
@@ -79,6 +83,11 @@ export class FailedSteps {
|
||||
* a kept failure's step clears it, and nothing else does.
|
||||
*/
|
||||
stepEnded(ev: StepEndedEvent): Promise<void> {
|
||||
// The `seen` keys past their 30 days are judged on each step end, with no timer (§581).
|
||||
return this.record(ev).then(() => this.expireSeen(new Date(Date.parse(String(ev?.at ?? "")) || Date.now())).then(() => {}));
|
||||
}
|
||||
|
||||
private record(ev: StepEndedEvent): Promise<void> {
|
||||
const r = ev?.result;
|
||||
const fingerprint = r?.fingerprint || ev?.fp || "";
|
||||
if (!r || !SHA.test(String(ev.commit ?? "")) || !fingerprint || !ev.module) return Promise.resolve();
|
||||
@@ -163,6 +172,21 @@ export class FailedSteps {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the `seen` keys whose `at` is more than 30 days before `now`, the oldest first, at most `most` of them (§581): run on
|
||||
* each `step-ended`, at the start, and by the tool `git_seen_expire`. A key with no readable `at` is kept. Answers the keys deleted.
|
||||
*/
|
||||
expireSeen(now: Date = new Date(), most = SEEN_MOST_PER_EVENT): Promise<string[]> {
|
||||
return this.turn(async () => {
|
||||
const { seen } = await this.all();
|
||||
const limit = now.getTime() - SEEN_DAYS * 24 * 3600 * 1000;
|
||||
const old = [...seen].filter(([, s]) => Date.parse(s.at) < limit).sort(([, a], [, b]) => Date.parse(a.at) - Date.parse(b.at)).slice(0, most);
|
||||
for (const [k] of old) await this.st.delete(k);
|
||||
if (old.length) this.log(`${old.length} seen key(s) past ${SEEN_DAYS} days deleted`);
|
||||
return old.map(([k]) => k);
|
||||
});
|
||||
}
|
||||
|
||||
/** Drops one held key, a failure by its key or a `seen` key; false when it is not there. */
|
||||
drop(key: string): Promise<boolean> {
|
||||
return this.turn(async () => {
|
||||
|
||||
@@ -402,6 +402,9 @@ const heldHeads = new Heads();
|
||||
let reloadHeads: () => Promise<void> = async () => {};
|
||||
const failedSteps = new FailedSteps(state<unknown>("failed-steps"), (line) => console.log(`[gitea] ${line}`), (commit) => heldHeads.has(commit));
|
||||
|
||||
// The `seen` keys past their 30 days are deleted at the start, as on each step-ended (design 47 §581).
|
||||
void failedSteps.expireSeen().catch((err) => console.log(`[gitea] the seen keys past 30 days could not be deleted at the start: ${err instanceof Error ? err.message : String(err)}`));
|
||||
|
||||
// gitea_events is served whether or not the forge is watched: it says so (novox/hq issue 729). The seat git's verbs are
|
||||
// served here, beside the one writer of a head's statuses: the seat's status takes its turn on the head's chain, after the
|
||||
// announcement's pending and a trunk's move, so a recheck's pending never lands over a later verdict (issue 456).
|
||||
|
||||
@@ -96,3 +96,25 @@ test("git_failed_steps shows what is held and its fill, and git_failed_steps_dro
|
||||
assert.deepEqual([done.dropped, acts.length, acts[0].verb], [true, 1, "git_failed_steps_drop"]);
|
||||
assert.equal(await f.judge(C2), null);
|
||||
});
|
||||
|
||||
test("a seen key past 30 days is deleted at the next step-ended, a younger one kept, at most 100 oldest first, and git_seen_expire runs the same now (§581)", async () => {
|
||||
const { failedStepsTools } = await import("../tools/failedsteps.js");
|
||||
const { runAs } = await import("@novox/mesh-sdk/callers");
|
||||
const { st, kv } = memory() as any;
|
||||
const f = new FailedSteps(st as any);
|
||||
// The step-ended is at 2026-11-11T12:00:00Z, so 30 days before it is 2026-10-12.
|
||||
const now = new Date("2026-11-11T12:00:00Z");
|
||||
const old = (n: number) => new Date(Date.parse("2026-10-01T00:00:00Z") - n * 60000).toISOString();
|
||||
await st.put(seenKey(C1, "old"), { v: 1, commit: C1, module: "old", fingerprints: ["x"], at: old(0) });
|
||||
await st.put(seenKey(C2, "young"), { v: 1, commit: C2, module: "young", fingerprints: ["x"], at: "2026-10-20T00:00:00Z" });
|
||||
await f.stepEnded(ended(C3, "passed"));
|
||||
assert.equal(kv.has(seenKey(C1, "old")), false, "past 30 days: deleted at the next step-ended");
|
||||
assert.equal(kv.has(seenKey(C2, "young")), true, "younger: kept");
|
||||
assert.equal(kv.has(seenKey(C3, "nats")), true, "the step's own key is new");
|
||||
for (let i = 0; i < 101; i++) await st.put(seenKey(C1, `m${String(i).padStart(3, "0")}`), { v: 1, commit: C1, module: `m${i}`, fingerprints: ["x"], at: old(i) });
|
||||
assert.equal((await f.expireSeen(now)).length, 100, "at most 100 for each event");
|
||||
const acts: any[] = [];
|
||||
const tool = failedStepsTools(f, async (a) => { acts.push(a); }).find((t) => t.name === "git_seen_expire")!;
|
||||
const done: any = await runAs({ "mesh/caller": "operator" }, {}, () => tool.run({ why: "stale" }));
|
||||
assert.deepEqual([done.deleted.length, acts[0].verb], [1, "git_seen_expire"]);
|
||||
});
|
||||
|
||||
@@ -40,5 +40,23 @@ export function failedStepsTools(kept: FailedSteps, acts: (act: HandAct) => Prom
|
||||
return { dropped: await kept.drop(key), key, why, by };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "git_seen_expire",
|
||||
description: "Deletes the seen keys of the state failed-steps that are more than 30 days old, the oldest first, at most 100: the removal the forge module does on each step-ended, now, with why. " +
|
||||
"Written in the hand-act log first (design 47 §581).",
|
||||
input: { type: "object", required: ["why"], properties: { why: { type: "string", description: "why it is run now, in plain words" } } },
|
||||
run: async (args: Record<string, unknown>) => {
|
||||
const why = typeof args.why === "string" ? args.why.trim() : "";
|
||||
if (!why) throw new Error("git_seen_expire says why");
|
||||
const by = callerOf(args);
|
||||
try {
|
||||
await acts({ what: "deleted the seen keys of the forge module's failed-steps past 30 days", why: `${why} (${by})`, cause: "seen-expired", verb: "git_seen_expire" });
|
||||
} catch (err) {
|
||||
throw new Error(`the hand act was not recorded, so nothing was deleted: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
const deleted = await kept.expireSeen();
|
||||
return { deleted, why, by };
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user