Phase 19 wiring + Path 1/2 work + chain integrity fixes
Backend:
- crates/vectord/src/playbook_memory.rs (new): Phase 19 in-memory boost
store with seed/rebuild/snapshot, plus temporal decay (e^-age/30 per
playbook), persist_to_sql endpoint backing successful_playbooks_live,
and discover_patterns endpoint for meta-index pattern aggregation
(recurring certs/skills/archetype/reliability across similar past fills).
- DEFAULT_TOP_K_PLAYBOOKS bumped 5 → 25; old default silently missed
most boosts when memory had > 25 entries.
- service.rs: new routes /vectors/playbook_memory/{seed,rebuild,stats,
persist_sql,patterns}.
Bun staffing co-pilot (mcp-server/):
- /search, /match, /verify, /proof, /simulation/run, MCP tools all
forward use_playbook_memory:true and playbook_memory_k:25 to the
hybrid endpoint. Boost was previously dark across the entire app.
- /log no longer POSTs to /ingest/file — that endpoint REPLACES the
dataset's object list, so single-row CSV writes were wiping all prior
rows in successful_playbooks (sp_rows went 33→1 in one /log call).
/log now seeds playbook_memory with canonical short text and calls
/persist_sql to keep successful_playbooks_live in sync.
- /simulation/run cumulative end-of-week CSV write removed for the same
reason. Per-day per-contract /seed (added in this session) is the
accumulating feedback path now.
- search.html addWorkerInsight renders a green "Endorsed · N playbooks"
chip with playbook citations when boost > 0.
Internal Dioxus UI (crates/ui/):
- Dashboard phase list rewritten through Phase 19 (was stuck at "Phase
16: File Watcher" / "Phase 17: DB Connector" — both wrong).
- Removed fabricated "27ms" stat label.
- Ask tab examples + SQL default replaced with real staffing prompts
against candidates/clients/job_orders (was referencing nonexistent
employees/products/events).
- New Playbook tab exposes /vectors/playbook_memory/{stats,rebuild} and
side-by-side hybrid search (boost OFF vs ON) with citations.
Tests (tests/multi-agent/):
- run_e2e_rated.ts: parallel two-agent (mistral + qwen2.5) build phase
+ verifier rating (geo, auth, persist, boost, speed → /10).
- network_proving.ts: continuous build → verify → repeat with
staffing-recruiter profile hot-swap; geo-discrimination check.
- chain_of_custody.ts: single recruiter operation traced through every
layer (Bun /search, direct /vectors/hybrid parity, /log, SQL,
playbook_memory growth, profile activation, post-op boost lift).
This commit is contained in:
+108
-32
@@ -74,7 +74,10 @@ server.tool(
|
||||
top_k: z.number().default(5),
|
||||
},
|
||||
async ({ question, sql_filter, dataset, id_column, top_k }) => {
|
||||
const body: any = { question, index_name: "workers_500k_v1", filter_dataset: dataset, id_column, top_k, generate: true };
|
||||
const body: any = {
|
||||
question, index_name: "workers_500k_v1", filter_dataset: dataset, id_column, top_k, generate: true,
|
||||
use_playbook_memory: true,
|
||||
};
|
||||
if (sql_filter) body.sql_filter = sql_filter;
|
||||
const r = await api("POST", "/vectors/hybrid", body);
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(r, null, 2) }] };
|
||||
@@ -109,6 +112,7 @@ server.tool(
|
||||
index_name: "workers_500k_v1", sql_filter: filter,
|
||||
filter_dataset: "ethereal_workers", id_column: "worker_id",
|
||||
top_k: headcount * 2, generate: false,
|
||||
use_playbook_memory: true,
|
||||
});
|
||||
let matches = r.sources || [];
|
||||
if (required_certs.length > 0) {
|
||||
@@ -384,6 +388,11 @@ async function main() {
|
||||
question: b.question, index_name: b.index || "workers_500k_v1",
|
||||
sql_filter: b.sql_filter, filter_dataset: b.dataset || "ethereal_workers",
|
||||
id_column: b.id_column || "worker_id", top_k: b.top_k || 5, generate: b.generate !== false,
|
||||
use_playbook_memory: b.use_playbook_memory !== false,
|
||||
// Forward explicitly so Bun /search isn't capped by the
|
||||
// server's default — boost silently misses good matches when
|
||||
// memory has >25 entries and only top-5 playbooks are scanned.
|
||||
playbook_memory_k: b.playbook_memory_k ?? 25,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -403,6 +412,8 @@ async function main() {
|
||||
index_name: b.index || "workers_500k_v1", sql_filter: filter,
|
||||
filter_dataset: b.dataset || "ethereal_workers",
|
||||
id_column: "worker_id", top_k: (b.headcount || 5) * 2, generate: false,
|
||||
use_playbook_memory: true,
|
||||
playbook_memory_k: 25,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -418,14 +429,57 @@ async function main() {
|
||||
return ok(await api("POST", "/vectors/rag", { index_name: b.index || "workers_500k_v1", question: b.question, top_k: b.top_k || 5 }));
|
||||
}
|
||||
|
||||
// Tool: log success
|
||||
// Tool: log success.
|
||||
//
|
||||
// BUG FIX 2026-04-20: previously this also POSTed a 1-row CSV to
|
||||
// /ingest/file?name=successful_playbooks. That endpoint REPLACES
|
||||
// the dataset's object list rather than appending — so every /log
|
||||
// call destroyed all prior rows in the SQL-queryable
|
||||
// successful_playbooks table. Chain-of-custody trace caught it:
|
||||
// sp_rows went 33 → 1 in a single /log call.
|
||||
//
|
||||
// Until a proper append endpoint exists (Phase 8 delta write
|
||||
// surface for the SQL table), /log writes ONLY to playbook_memory
|
||||
// (in-memory append-only store, works correctly for boost). The
|
||||
// SQL successful_playbooks table is now treated as derived state
|
||||
// that gets rebuilt explicitly via /vectors/playbook_memory/rebuild
|
||||
// — never written to by the recruiter path.
|
||||
if (url.pathname === "/log") {
|
||||
const b = await json();
|
||||
const csv = `timestamp,operation,approach,result,context\n"${new Date().toISOString()}","${(b.operation||"").replace(/"/g,'""')}","${(b.approach||"").replace(/"/g,'""')}","${(b.result||"").replace(/"/g,'""')}","${(b.context||"").replace(/"/g,'""')}"`;
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([csv], { type: "text/csv" }), "playbook.csv");
|
||||
const r = await fetch(`${BASE}/ingest/file?name=successful_playbooks`, { method: "POST", body: form });
|
||||
return ok({ logged: true, response: await r.text() });
|
||||
// Result format expected: "{filled}/{needed} filled → Name1, Name2, Name3"
|
||||
const result = String(b.result || "");
|
||||
const arrowIdx = result.indexOf("→");
|
||||
const namesPart = arrowIdx >= 0 ? result.slice(arrowIdx + 1) : "";
|
||||
const endorsed = namesPart.split(",").map(s => s.trim()).filter(Boolean);
|
||||
let seeded = 0;
|
||||
let persisted_rows = 0;
|
||||
if (endorsed.length && /fill:.+ in .+,.+/i.test(String(b.operation || ""))) {
|
||||
const canonicalApproach = `${(b.approach || "manual log").split(/[\.\n]/)[0]}`.slice(0, 80);
|
||||
const canonicalContext = `${(b.context || "").split(/[\.\n]/)[0]}`.slice(0, 80);
|
||||
const seedRes = await api("POST", "/vectors/playbook_memory/seed", {
|
||||
operation: b.operation,
|
||||
approach: canonicalApproach,
|
||||
context: canonicalContext,
|
||||
endorsed_names: endorsed,
|
||||
append: true,
|
||||
}).catch(() => null) as any;
|
||||
if (seedRes && seedRes.playbook_id) {
|
||||
seeded = endorsed.length;
|
||||
// After every successful seed, persist memory → SQL so the
|
||||
// successful_playbooks_live table reflects current operator
|
||||
// activity. /persist_sql writes the FULL state, which is safe
|
||||
// because in-memory playbook_memory IS the source of truth
|
||||
// (no concurrent writer outside this process modifies it).
|
||||
const pr = await api("POST", "/vectors/playbook_memory/persist_sql", {}).catch(() => null) as any;
|
||||
if (pr && typeof pr.rows_persisted === "number") persisted_rows = pr.rows_persisted;
|
||||
}
|
||||
}
|
||||
return ok({
|
||||
logged: true,
|
||||
seeded,
|
||||
persisted_to_sql: persisted_rows,
|
||||
note: "successful_playbooks_live (NOT successful_playbooks) is the SQL surface for live operator activity. /log is non-destructive.",
|
||||
});
|
||||
}
|
||||
|
||||
// Tool: get playbooks
|
||||
@@ -480,6 +534,7 @@ async function main() {
|
||||
question: "reliable forklift operator", index_name: "workers_500k_v1",
|
||||
sql_filter: "role = 'Forklift Operator' AND state = 'IL' AND CAST(reliability AS DOUBLE) > 0.8",
|
||||
filter_dataset: "workers_500k", id_column: "worker_id", top_k: 5, generate: false,
|
||||
use_playbook_memory: true,
|
||||
});
|
||||
tests.push({
|
||||
name: "Hybrid SQL+Vector Search", ms: Date.now() - ht0,
|
||||
@@ -987,6 +1042,7 @@ tr:hover{background:#111827}
|
||||
question: "reliable forklift operator", index_name: "workers_500k_v1",
|
||||
sql_filter: "role = 'Forklift Operator' AND state = 'IL' AND CAST(reliability AS DOUBLE) > 0.8",
|
||||
filter_dataset: "workers_500k", id_column: "worker_id", top_k: 5, generate: false,
|
||||
use_playbook_memory: true,
|
||||
});
|
||||
tests.push({
|
||||
name: "Hybrid SQL+Vector", ms: Date.now() - ht0,
|
||||
@@ -1435,6 +1491,26 @@ const SCENARIOS = [
|
||||
|
||||
function pick<T>(arr: T[]): T { return arr[Math.floor(Math.random() * arr.length)]; }
|
||||
|
||||
// Seed playbook_memory from a filled contract so the next hybrid query
|
||||
// ranks against it. Used by both runWeekSimulation (per-day) and the /log
|
||||
// endpoint (per manual logging). Fail-soft — seeding is best-effort.
|
||||
async function seedPlaybookFromContract(c: any) {
|
||||
const names = (c.matches || []).slice(0, 5)
|
||||
.map((m: any) => m.name || m.doc_id)
|
||||
.filter((n: string) => n && !n.startsWith("W500-"));
|
||||
if (!names.length) return;
|
||||
const op = `fill: ${c.role} x${c.headcount} in ${c.city}, ${c.state}`;
|
||||
try {
|
||||
await api("POST", "/vectors/playbook_memory/seed", {
|
||||
operation: op,
|
||||
approach: `${c.situation || c.priority || "fill"} → hybrid search`,
|
||||
context: `client=${c.client || ""} start=${c.start || ""}`,
|
||||
endorsed_names: names,
|
||||
append: true,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function runWeekSimulation() {
|
||||
const days = ["Monday","Tuesday","Wednesday","Thursday","Friday"];
|
||||
const staffers = ["Sarah (Lead)","Mike (Senior)","Kim (Junior)"];
|
||||
@@ -1468,7 +1544,7 @@ async function runWeekSimulation() {
|
||||
if (priority === "urgent") emergencies++;
|
||||
totalNeeded += headcount;
|
||||
|
||||
// Run hybrid search
|
||||
// Run hybrid search — Phase 19: boost on so past playbooks shape ranking
|
||||
let filled = 0;
|
||||
let matches: any[] = [];
|
||||
try {
|
||||
@@ -1481,12 +1557,15 @@ async function runWeekSimulation() {
|
||||
id_column: "worker_id",
|
||||
top_k: headcount + 2,
|
||||
generate: false,
|
||||
use_playbook_memory: true,
|
||||
});
|
||||
matches = (r.sources || []).slice(0, headcount).map((s: any) => ({
|
||||
doc_id: s.doc_id,
|
||||
name: s.chunk_text?.split("—")[0]?.trim() || s.doc_id,
|
||||
score: s.score,
|
||||
chunk_text: s.chunk_text || "",
|
||||
playbook_boost: s.playbook_boost || 0,
|
||||
playbook_citations: s.playbook_citations || [],
|
||||
}));
|
||||
filled = matches.length;
|
||||
} catch {}
|
||||
@@ -1501,7 +1580,15 @@ async function runWeekSimulation() {
|
||||
});
|
||||
}
|
||||
|
||||
// End of day: log playbook + prepare handoff
|
||||
// End of day: seed playbook_memory with TODAY's filled contracts so
|
||||
// tomorrow's hybrid search ranks against them. This is the in-week
|
||||
// feedback loop — without this, day 5 doesn't benefit from day 1.
|
||||
for (const c of contracts) {
|
||||
if (c.matches && c.matches.length) {
|
||||
await seedPlaybookFromContract(c).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
if (d < 4) {
|
||||
handoffs++;
|
||||
try {
|
||||
@@ -1530,29 +1617,18 @@ async function runWeekSimulation() {
|
||||
playbook_entries: playbookEntries,
|
||||
};
|
||||
|
||||
// Log every filled contract as a playbook entry — this is the training data
|
||||
try {
|
||||
const ts = new Date().toISOString();
|
||||
const rows: string[] = [];
|
||||
for (const day of results) {
|
||||
for (const c of day.contracts) {
|
||||
if (c.matches && c.matches.length > 0) {
|
||||
const workerNames = c.matches.slice(0, 3).map((m: any) => m.name || m.doc_id).join(", ");
|
||||
const op = `fill: ${c.role} x${c.headcount} in ${c.city}, ${c.state}`;
|
||||
const approach = `${c.situation} (${c.priority}) → hybrid search`;
|
||||
const result = `${c.filled}/${c.headcount} filled → ${workerNames}`;
|
||||
const context = `client=${c.client} start=${c.start} scenario=${c.situation}`;
|
||||
rows.push(`"${ts}","${op.replace(/"/g,'""')}","${approach}","${result.replace(/"/g,'""')}","${context.replace(/"/g,'""')}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rows.length) {
|
||||
const csv = `timestamp,operation,approach,result,context\n${rows.join("\n")}`;
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([csv], { type: "text/csv" }), "playbook.csv");
|
||||
await fetch(`${BASE}/ingest/file?name=successful_playbooks`, { method: "POST", body: form });
|
||||
}
|
||||
} catch {}
|
||||
// BUG FIX 2026-04-20: previously this POSTed a multi-row CSV to
|
||||
// /ingest/file?name=successful_playbooks at end of every simulation.
|
||||
// That endpoint REPLACES the dataset's object list — so each
|
||||
// /simulation/run wiped the prior simulation's rows. The SQL
|
||||
// successful_playbooks table was never accumulating; it always reflected
|
||||
// only the most-recent simulation batch.
|
||||
//
|
||||
// Per-day per-contract seeding via /vectors/playbook_memory/seed
|
||||
// (added Pass 1, runs inside the day loop above) is the path that
|
||||
// actually accumulates feedback. The SQL successful_playbooks table is
|
||||
// intentionally not written by /simulation/run anymore until a proper
|
||||
// append surface exists.
|
||||
|
||||
return { days: results, summary };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user