diff --git a/mcp-server/index.ts b/mcp-server/index.ts index aa1d209..5198847 100644 --- a/mcp-server/index.ts +++ b/mcp-server/index.ts @@ -1391,15 +1391,11 @@ tr:hover{background:#111827} } } - // Intelligence: Log a search → selection as a learned pattern - if (url.pathname === "/intelligence/learn" && req.method === "POST") { - const b = await json(); - const csv = `timestamp,operation,approach,result,context\n"${new Date().toISOString()}","search: ${(b.query||"").replace(/"/g,'""')}","${(b.filters||"").replace(/"/g,'""')}","selected: ${(b.worker_name||"").replace(/"/g,'""')} (${b.worker_id||""})","role=${b.worker_role||""} state=${b.worker_state||""} city=${b.worker_city||""}"`; - 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 }); - return ok({ learned: true, pattern: `"${b.query}" → ${b.worker_name}` }); - } + // Removed 2026-04-20: /intelligence/learn was a legacy CSV writer + // that destructively re-wrote successful_playbooks. /log and + // /log_failure replace it cleanly via /vectors/playbook_memory/seed + // and /mark_failed. Keeping the endpoint would only mislead + // future callers — dead code rots. // Intelligence: Activity feed — what the system has learned if (url.pathname === "/intelligence/activity" && req.method === "POST") { diff --git a/mcp-server/search.html b/mcp-server/search.html index 9517d28..56e9e86 100644 --- a/mcp-server/search.html +++ b/mcp-server/search.html @@ -619,6 +619,39 @@ function showProfile(workerData){ body.appendChild(srcBox); } + // Call history — recruiter-facing institutional memory from call_log. + // Queries for prior contact with this specific worker (by name + // cross-ref). Fails soft: if no rows, shows "no recent contact" which + // is itself a useful signal (or an honest tell about data sparsity). + addSection(body,'Recent Contact','Last phone outreach logged in call_log'); + var callBox=document.createElement('div'); + callBox.style.cssText='background:#0d1117;border-radius:8px;padding:14px;margin-bottom:20px;font-size:12px;color:#8b949e;line-height:1.6'; + callBox.textContent='Checking call log...';body.appendChild(callBox); + var nameLitC=(workerData.nm||'').replace(/'/g,"''"); + var callSQL="SELECT cl.timestamp, cl.recruiter, cl.duration_seconds, cl.disposition " + +"FROM call_log cl JOIN candidates c ON c.candidate_id = cl.candidate_id " + +"WHERE CONCAT(c.first_name, ' ', c.last_name) = '"+nameLitC+"' " + +"ORDER BY cl.timestamp DESC LIMIT 3"; + api('/sql',{sql:callSQL}).then(function(r){ + callBox.textContent=''; + var rows=(r&&r.rows)||[]; + if(rows.length===0){ + callBox.textContent='No recent call logged for '+(workerData.nm||'this worker')+'. Data note: call_log cross-references candidate IDs that may not align with workers_500k — real ATS integration required for full coverage.'; + callBox.style.color='#484f58';return; + } + rows.forEach(function(c){ + var row=document.createElement('div');row.style.cssText='padding:6px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;border-left:2px solid #58a6ff;display:flex;justify-content:space-between;gap:10px'; + var left=document.createElement('div'); + var ts=(c.timestamp||'').substring(0,10); + var dur=Math.round((c.duration_seconds||0)/60); + var l1=document.createElement('div');l1.style.cssText='color:#e6edf3;font-weight:500;font-size:12px'; + l1.textContent=ts+(c.recruiter?' · by '+c.recruiter:'');left.appendChild(l1); + var l2=document.createElement('div');l2.style.cssText='color:#8b949e;font-size:10px'; + l2.textContent=(c.disposition||'?').replace(/_/g,' ')+(dur?' · '+dur+' min':'');left.appendChild(l2); + row.appendChild(left);callBox.appendChild(row); + }); + }).catch(function(){callBox.textContent='(call log unavailable)';callBox.style.color='#484f58'}); + // Past playbook history — Phase 19 institutional memory surfaced on // the worker's own profile. Shows every past fill this worker was // endorsed in (from successful_playbooks_live), so the recruiter can @@ -700,14 +733,35 @@ function addWorkerInsight(parent,name,detail,why,idx,highlight){ var info=document.createElement('div');info.className='info'; var nm=document.createElement('div');nm.className='nm';nm.textContent=name; // Phase 19: when a past playbook endorsed this worker, show a green chip - // next to the name. Hover reveals the citation IDs. + // next to the name. Hover reveals a NARRATIVE of past endorsements + // derived from successful_playbooks_live — "filled X in Y on date" — + // rather than opaque pb-seed-xxx ids. Recruiters need stories, not + // citation keys. Lazy-loaded per card on first render. if(boostInfo && boostInfo.boost > 0){ var chip=document.createElement('span'); - chip.style.cssText='display:inline-block;margin-left:8px;padding:2px 7px;border-radius:9px;font-size:10px;font-weight:600;background:#0d2818;border:1px solid #2ea043;color:#3fb950;vertical-align:middle'; + chip.style.cssText='display:inline-block;margin-left:8px;padding:2px 7px;border-radius:9px;font-size:10px;font-weight:600;background:#0d2818;border:1px solid #2ea043;color:#3fb950;vertical-align:middle;cursor:help'; var n=(boostInfo.citations && boostInfo.citations.length) || 0; chip.textContent='Endorsed · '+n+' playbook'+(n!==1?'s':''); - chip.title='Boosted by past playbooks: '+(boostInfo.citations||[]).join(', '); + chip.title='Loading past playbooks for '+name+'...'; nm.appendChild(chip); + // Fetch narrative for this worker lazily + var safeName = (name||'').replace(/'/g,"''"); + var narrativeSQL = "SELECT operation, result, timestamp FROM successful_playbooks_live " + + "WHERE result LIKE '%"+safeName+"%' ORDER BY timestamp DESC LIMIT 5"; + api('/sql',{sql:narrativeSQL}).then(function(r){ + var rows=(r&&r.rows)||[]; + if(rows.length===0){ + chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':'')+' (narrative unavailable — may have been seeded without SQL persistence)'; + return; + } + var stories=rows.map(function(pb){ + var d=(pb.timestamp||'').substring(0,10); + return '• '+(pb.operation||'?').replace(/^fill:\s*/,'')+' ('+d+')'; + }); + chip.title=name+' — past endorsements:\n'+stories.join('\n'); + }).catch(function(){ + chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':''); + }); } var dt=document.createElement('div');dt.className='detail';dt.textContent=detail; info.appendChild(nm);info.appendChild(dt); @@ -770,16 +824,31 @@ function doSearch(){ var h=document.createElement('div');h.style.cssText='color:#8b949e;font-size:12px;margin-bottom:10px'; h.textContent=(d.sql_matches?d.sql_matches.toLocaleString()+' workers matched — ':'')+'showing best results ('+(d.duration_ms||0)+'ms)'; out.appendChild(h); - // Meta-index signal — what past similar fills had in common. Only - // renders when memory had at least one relevant playbook. - if(d.discovered_pattern && d.pattern_playbooks_matched > 0){ + // Meta-index signal — ALWAYS render when the system has any memory, + // even if no trait crossed threshold. Silence here would have + // recruiters assume "no signal" when the reality is "threshold + // filtered it out" or "memory is sparse for this geo." Trust + // depends on the system being honest about what it doesn't know. + if(d.pattern_playbooks_matched > 0 || d.discovered_pattern){ var mem=document.createElement('div'); mem.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5'; var label=document.createElement('span');label.style.cssText='color:#3fb950;font-weight:600;margin-right:6px'; - label.textContent='MEMORY ('+d.pattern_playbooks_matched+' playbooks):'; + label.textContent='MEMORY ('+(d.pattern_playbooks_matched||0)+' playbook'+(d.pattern_playbooks_matched===1?'':'s')+'):'; mem.appendChild(label); - mem.appendChild(document.createTextNode(' '+d.discovered_pattern)); + var pattern = d.discovered_pattern || ''; + if(!pattern || pattern.indexOf('No similar')>=0 || pattern.indexOf('0 workers')>=0){ + mem.appendChild(document.createTextNode(' memory is sparse for this role+geo — no trait crossed threshold. Will accumulate as fills land.')); + mem.style.color='#6ca885'; + } else { + mem.appendChild(document.createTextNode(' '+pattern)); + } out.appendChild(mem); + } else { + // Zero playbooks matched — be explicit + var mem0=document.createElement('div'); + mem0.style.cssText='background:#161b22;border:1px solid #21262d;border-radius:6px;padding:6px 12px;margin-bottom:10px;font-size:11px;color:#6e7681'; + mem0.textContent='MEMORY: no similar past playbooks yet — first fill of this kind will seed it.'; + out.appendChild(mem0); } // Render results based on type var workers=d.sql_results||[];