Files
ThothII/harness/.pi/extensions/gate/memory/index.js
T
Codex d8a29bfbdd Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
2026-09-13 14:26:39 +02:00

233 lines
9.3 KiB
JavaScript

import { Type } from "typebox";
import { installRepairGate } from "./repair.js";
// The composition root supplies manifest language; only deterministic chrome is localized.
function normalizedText(value) {
return String(value ?? "").trim().replace(/\s+/g, " ").toLowerCase();
}
const MEMORY_ID_RE = /\bmem-(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\d{4,})\b/i;
function memoryOptionKey(option) {
const searchable = [
option.id,
option.label,
option.description,
option.decision?.rationale,
].join(" ");
const memoryId = searchable.match(MEMORY_ID_RE)?.[0]?.toLowerCase();
if (memoryId) return `id:${memoryId}`;
return [
"content",
normalizedText(option.decision?.type),
normalizedText(option.decision?.subject),
normalizedText(option.decision?.detail),
].join(":");
}
function normalizeMemoryOptions(options, locale, gateText) {
const seen = new Set();
const out = [];
for (const option of options) {
if (option.decision?.type !== "concept_clarified") continue;
const key = memoryOptionKey(option);
if (seen.has(key)) continue;
seen.add(key);
const memoryId = [
option.id,
option.label,
option.description,
option.decision?.rationale,
].join(" ").match(MEMORY_ID_RE)?.[0]?.toLowerCase();
const fallbackDetail = [
option.decision?.type && option.decision?.subject
? `${gateText(locale, "decision")} ${option.decision.type}: ${option.decision.subject}`
: "",
option.decision?.detail ?? "",
].filter(Boolean).join("\n");
out.push({
...option,
detail: option.description?.trim() || fallbackDetail,
rationale: option.decision?.rationale ?? "",
...(memoryId ? { meta: { memory_id: memoryId } } : {}),
});
}
return out;
}
function memorySelectionWidgetProps(options, locale, gateText) {
return {
title: gateText(locale, "memoryTitle"),
titleI18n: gateText("en", "memoryTitle"),
selected: options.filter((option) => option.recommended).map((option) => option.id),
selectionLabel: gateText(locale, "memorySelection"),
confirmLabel: gateText(locale, "memoryApply"),
selectionLabelI18n: gateText("en", "memorySelection"),
confirmLabelI18n: gateText("en", "memoryApply"),
};
}
function shouldSkipEmptyRecall({ meritCount, allowEmpty, advance }) {
return meritCount === 0 && !!allowEmpty && !!advance;
}
async function reviewRecall(ctx, params, phase, dependencies) {
const { workflow, ledger, reviewer, waitForReviewer, toTextResult, languageFor, gateText } = dependencies;
try {
const { session, advance } = params;
const locale = languageFor(ctx, session);
const options = normalizeMemoryOptions(params.options, locale, gateText);
const typeError = ledger.validate(ctx, options, session);
if (typeError) return toTextResult(typeError);
const meritOptions = options
.filter((option) => !reviewer.isReserved(option.label))
.map((option) => ({
id: option.id,
label: option.label,
...(option.detail ? { detail: option.detail } : {}),
...(option.rationale ? { rationale: option.rationale } : {}),
...(option.meta ? { meta: option.meta } : {}),
}));
if (shouldSkipEmptyRecall({
meritCount: meritOptions.length,
allowEmpty: params.allow_empty ?? false,
advance,
})) {
await ctx.ui.notify(
gateText(locale, "memoryEmpty"),
"info",
);
workflow.advance(ctx, session);
return toTextResult(
"Fase memoria vuota: nessuna decisione da registrare, " +
"avanzamento automatico alla fase successiva.",
);
}
const widget = reviewer.buildMultiselect({
id: `u${Date.now()}`,
phase,
allowEmpty: params.allow_empty ?? false,
options: meritOptions,
...memorySelectionWidgetProps(options, locale, gateText),
});
const response = await waitForReviewer(ctx, widget);
if (response.control === "freetext") {
return toTextResult(
`Altro (reviewer): ${response.text}. Riformula la proposta tenendo conto.`,
);
}
if (response.control === "back") {
return toTextResult("Il reviewer vuole tornare indietro.");
}
if (response.control === "exit") {
return toTextResult("Il reviewer vuole uscire.");
}
const chosen = options.filter((option) =>
(response.choices ?? []).includes(option.id));
const recorded = [];
for (const choice of chosen) {
const error = ledger.record(ctx, session, choice.decision, "");
if (error) return error;
recorded.push(choice.decision);
}
if (!recorded.length) {
const result = advance ? workflow.advance(ctx, session) : { advanced: false };
const message =
"Nessuna decisione registrata (il reviewer non ha selezionato opzioni di merito).";
return toTextResult(
result.advanced ? message + " Fase avanzata automaticamente." : message,
);
}
const result = advance ? workflow.advance(ctx, session) : { advanced: false };
const parts = [
`Registrate ${recorded.length} decisioni: ` +
recorded.map((decision) => decision.type).join(", ") + ".",
];
parts.push(result.advanced
? "Fase avanzata automaticamente."
: 'La fase resta aperta: chiudila con reviewer_confirm kind:"phase" quando pronta.');
return toTextResult(parts.join(" "));
} catch (fatal) {
const message = (fatal.stderr || fatal.message || String(fatal)).toString().trim();
return toTextResult(
`[reviewer_decide ERRORE INTERNO] ${message}. Riprova o usa un approccio diverso.`,
);
}
}
function installMemoryGate(pi, { workflow, memory, ledger, waitForReviewer, toTextResult, languageFor, gateText }) {
pi.registerTool({
name: "reviewer_memory_promote",
label: "Review Memory summary",
description: "F8: show the editable Memory summary from persisted proposals and approved " +
"decisions. The reviewer selects additions, updates and links. Only those choices " +
"are saved. Then close F8 and finalize. Prepare reusable rules during the workflow " +
"with tht memory propose; do not call review-apply yourself.",
parameters: Type.Object({ session: Type.String() }),
async execute(_id, params, _signal, _onUpdate, ctx) {
workflow.activate();
try {
const { session } = params;
const phase = workflow.phase(ctx, session);
if (phase.number > 8) return workflow.close(ctx, session, phase.number, "Memory reviewed.");
if (phase.number !== 8) return toTextResult("Review the Memory summary at the end of F8.");
const summary = JSON.parse(memory.execute(ctx, ["summary", "--session", session, "--json"]));
if (summary.reviewed) {
const failure = ledger.record(ctx, session, {
type: "memory_summary_reviewed", subject: summary.summary_id,
detail: "Previously saved Memory review recovered.",
}, "Retry to record the recovered Memory review.");
if (failure) return failure;
return workflow.close(ctx, session, phase.number, "Memory review recovered.");
}
const response = await waitForReviewer(ctx, {
id: `u${Date.now()}`, schema_version: 1, phase: phase.id,
widget: "memory-review", title: gateText(languageFor(ctx, session), "memorySummary"),
title_i18n: gateText("en", "memorySummary"),
summary, reserved: ["other", "back", "exit"],
});
if (response.control) return toTextResult(
response.control === "freetext"
? `Reviewer feedback: ${response.text}. Revise the proposals and present the summary again.`
: `Reviewer requested: ${response.control}.`);
const selection = JSON.parse(response.text || "{}");
if (selection.summary_id !== summary.summary_id || !Array.isArray(selection.items))
return toTextResult("Invalid Memory review response. Present the summary again.");
const failure = await memory.mutate(ctx, [
"review-apply", "--session", session, "--review-json", JSON.stringify(selection), "--json",
], "The Memory review is recoverable. Present the summary again if its content changed.");
if (failure) return failure;
const selected = new Set(selection.items.map(item => item.id));
const ledgerFailure = ledger.record(ctx, session, {
type: "memory_summary_reviewed", subject: summary.summary_id,
detail: `Saved ${selected.size} cards; declined ${summary.items.length-selected.size}.`,
}, "Memory is saved. Retry to record the review and finalize.");
if (ledgerFailure) return ledgerFailure;
const message = `Memory reviewed: ${selected.size} cards saved, ${summary.items.length-selected.size} declined.`;
return workflow.close(ctx, session, phase.number, message) || toTextResult(message);
} catch (error) {
return toTextResult(`[reviewer_memory_promote] ${String(error.stderr || error.message || error).trim()}`);
}
},
});
}
export function createMemoryGate(dependencies) {
return {
install: (pi) => {
installMemoryGate(pi, dependencies);
installRepairGate(pi, dependencies);
},
reviewRecall: (ctx, params, phase) => reviewRecall(ctx, params, phase, dependencies),
};
}