fix: clarify memory selection semantics
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { memorySelectionWidgetProps } = require("../../tht-gate.js");
|
||||
|
||||
test("F2 preseleziona solo le memory raccomandate e parla di applicazione", () => {
|
||||
assert.deepEqual(
|
||||
memorySelectionWidgetProps([
|
||||
{ id: "recommended", label: "Memory A", recommended: true },
|
||||
{ id: "optional", label: "Memory B" },
|
||||
]),
|
||||
{
|
||||
title: "Seleziona le memory da applicare alla domanda",
|
||||
selected: ["recommended"],
|
||||
selectionLabel: "memory da applicare",
|
||||
confirmLabel: "Applica le memory selezionate",
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -7,6 +7,13 @@ const { createFakePi } = require("./fake_pi_runtime.js");
|
||||
const installGate = require("../../tht-gate.js").default ?? require("../../tht-gate.js");
|
||||
|
||||
test("con THT_SESSION il kickoff usa l'id fornito e NON crea la sessione", async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tht-gate-no-pack-"));
|
||||
const bin = path.join(tmp, "bin");
|
||||
fs.mkdirSync(bin);
|
||||
const tht = path.join(bin, "tht");
|
||||
fs.writeFileSync(tht, "#!/bin/sh\nexit 1\n", { mode: 0o755 });
|
||||
const oldPath = process.env.PATH;
|
||||
process.env.PATH = bin + ":" + oldPath;
|
||||
process.env.THT_SESSION = "2026-06-27-100000-test";
|
||||
try {
|
||||
const { pi, ctx } = createFakePi();
|
||||
@@ -19,13 +26,15 @@ test("con THT_SESSION il kickoff usa l'id fornito e NON crea la sessione", async
|
||||
assert.doesNotMatch(text, /tht session new/);
|
||||
assert.match(text, /retrieval pack non era ancora disponibile/i);
|
||||
assert.match(text, /tht search pack/);
|
||||
assert.doesNotMatch(text, /<retrieval-pack>/);
|
||||
assert.doesNotMatch(text, /<retrieval-pack>\s*# Retrieval pack/);
|
||||
assert.match(text, /<tht-sessione-skill>/);
|
||||
assert.match(text, /# Thoth session workflow \(phases 1-8\)/);
|
||||
assert.match(text, /non esplorare il repository/i);
|
||||
assert.doesNotMatch(text, /Carica la skill leggendo/);
|
||||
} finally {
|
||||
process.env.PATH = oldPath;
|
||||
delete process.env.THT_SESSION;
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ function buildSelectRequest({
|
||||
// `allowEmpty:false` with zero options is a broken widget and throws.
|
||||
function buildMultiselectRequest({
|
||||
id, phase, title, options, content = null, selected = [], allowEmpty = false,
|
||||
selectionLabel, confirmLabel,
|
||||
}) {
|
||||
requireString(title, "title", "multiselect");
|
||||
const opts = requireArray(options, "options", "multiselect");
|
||||
@@ -77,10 +78,12 @@ function buildMultiselectRequest({
|
||||
widget: "multiselect",
|
||||
title,
|
||||
allow_empty: allowEmpty,
|
||||
options: [...opts],
|
||||
options: opts.map((option) => ({ ...option, selected: selected.includes(option.id) })),
|
||||
selected: [...selected],
|
||||
content,
|
||||
reserved: RESERVED,
|
||||
...(selectionLabel ? { selection_label: selectionLabel } : {}),
|
||||
...(confirmLabel ? { confirm_label: confirmLabel } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -386,6 +386,17 @@ export function shouldSkipEmptyDecide({ meritCount, allowEmpty, advance }) {
|
||||
return meritCount === 0 && !!allowEmpty && !!advance;
|
||||
}
|
||||
|
||||
// Phase 2 uses a single positive checkbox meaning: apply this memory now.
|
||||
// Candidates not checked by the reviewer remain undecided and may be shown again.
|
||||
export function memorySelectionWidgetProps(options) {
|
||||
return {
|
||||
title: "Seleziona le memory da applicare alla domanda",
|
||||
selected: options.filter((option) => option.recommended).map((option) => option.id),
|
||||
selectionLabel: "memory da applicare",
|
||||
confirmLabel: "Applica le memory selezionate",
|
||||
};
|
||||
}
|
||||
|
||||
// --- F8 memory-promotion gate: pure candidate->widget mapping (L1-tested) ------
|
||||
// The candidates come from `tht memory promote --preview --json` (deterministic,
|
||||
// reviewer-approved decisions only); the model never authors them.
|
||||
@@ -730,10 +741,10 @@ export default function (pi) {
|
||||
const widget = buildMultiselectRequest({
|
||||
id: `u${Date.now()}`,
|
||||
phase,
|
||||
title,
|
||||
title: phase === "F2" ? memorySelectionWidgetProps(opts).title : title,
|
||||
allowEmpty: params.allow_empty ?? false,
|
||||
options: meritOptions,
|
||||
recommended: opts.find((o) => o.recommended)?.id ?? null,
|
||||
...(phase === "F2" ? memorySelectionWidgetProps(opts) : {}),
|
||||
});
|
||||
const resp = await emitAndWait(ctx, widget);
|
||||
if (resp.control === "freetext") {
|
||||
|
||||
@@ -215,21 +215,21 @@ Prerequisite: Phase 1 closed.
|
||||
allow_empty:true)`. Rules: at most **5** candidates; ONLY the 3 reusable types
|
||||
(`concept_clarified`, `table_promoted`, `table_excluded`) — query-specific
|
||||
decisions (`question_rewritten`, `sql_approved`, …) are NOT transferable, never
|
||||
propose them. Each option carries `mem_id:"mem-<id>"` plus `type`/`subject`/
|
||||
`rationale`. Selected options are applied (register the decision citing the
|
||||
`mem_id` in the rationale); deselected ones are recorded as `memory_rejected` by
|
||||
the gate (so the next `tht memory search --session` won't re-propose them). The
|
||||
checklist starts pre-selected with the recommended memories. With `allow_empty:true`
|
||||
an empty selection is accepted (no memory applied; deselected still recorded) and
|
||||
the phase advances — no separate gate.
|
||||
propose them. Each option carries `type`/`subject`/`rationale`; cite the source
|
||||
memory id (`mem-<id>`) in its rationale when applying it. Every option describes a
|
||||
candidate memory; never create an opposite "do not use" option. Only
|
||||
`recommended:true` options start checked. A
|
||||
deselected candidate is **not applied now**, not rejected, and may be considered
|
||||
again if Phase 2 is reopened. With `allow_empty:true` an empty selection is accepted
|
||||
(no memory applied) and the phase advances — no separate gate.
|
||||
When the memory search returned **zero** candidates, still issue the single
|
||||
`reviewer_decide(multi:true, advance:true, allow_empty:true)` with an empty merito list: the gate
|
||||
detects the empty+advance case, shows the reviewer an info notice ("Nessuna memory
|
||||
riutilizzabile … passo alla fase successiva") and auto-advances F2 — it does NOT present
|
||||
an empty checklist, and you do NOT add a separate `reviewer_confirm kind:"phase"`.
|
||||
4. Closing: if memories were applied or rejected (substantive decisions), `advance:true`
|
||||
no-ops — close with `reviewer_confirm kind:"phase"`. Only a truly empty memory phase
|
||||
(nothing applied, nothing rejected) auto-advances via `advance:true`.
|
||||
4. Closing: if one or more memories were applied (substantive decisions), `advance:true`
|
||||
no-ops — close with `reviewer_confirm kind:"phase"`. If none is applied, F2
|
||||
auto-advances via `advance:true`.
|
||||
5. Memories are promoted at the END of the workflow (Phase 8, the
|
||||
`reviewer_memory_promote` gate) — never promote from here, never run
|
||||
`tht memory promote`/`save-one` yourself (the gate blocks them).
|
||||
|
||||
@@ -24,14 +24,13 @@ Rules:
|
||||
(e.g. `question_rewritten`, `sql_approved`) are NOT to be proposed: they don't
|
||||
transfer to other questions.
|
||||
- All candidate memories go in **a single** `reviewer_decide(multi:true,
|
||||
advance:true, allow_empty:true)`: each selected option is applied (register the
|
||||
decision with the appropriate type, citing the memory id in the rationale), each
|
||||
deselected option is **recorded as `memory_rejected` by the gate** (so the next
|
||||
`tht memory search --session` won't re-propose it). To enable this, EVERY memory
|
||||
option MUST carry the field `mem_id:"mem-<id>"` (besides `type`/`subject`/
|
||||
`rationale`). The checklist starts pre-selected with the recommended memories.
|
||||
With `allow_empty:true` an **empty selection is accepted** (no memory applied; the
|
||||
deselected ones are still recorded as rejected) and the phase advances — no
|
||||
advance:true, allow_empty:true)`: every option describes a candidate memory, never
|
||||
an opposite action such as "do not use it". Each selected option is applied
|
||||
(register the decision with the appropriate type, citing the memory id in the
|
||||
rationale). Mark `recommended:true` ONLY on memories proposed for use: those, and
|
||||
only those, start checked. An unchecked memory is **not applied now**; it is not a
|
||||
rejection and may be considered again if Phase 2 is reopened. With `allow_empty:true`
|
||||
an **empty selection is accepted** (no memory applied) and the phase advances — no
|
||||
separate gate.
|
||||
- For "inspect": show the memory's full JSON record in the prose before presenting
|
||||
the checklist, if the reviewer asks.
|
||||
|
||||
Reference in New Issue
Block a user