Session infrastructure: OpenRouter + tree-split reducer + observer→LLM Team + scrum_applier #11
@ -27,7 +27,7 @@
|
||||
// Off by default: caller checks LH_AUDITOR_KIMI=1 before invoking.
|
||||
|
||||
import { readFile, writeFile, mkdir, appendFile, stat } from "node:fs/promises";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import type { Finding, CheckKind } from "../types.ts";
|
||||
|
||||
@ -265,31 +265,36 @@ function parseFindings(content: string): Finding[] {
|
||||
// is appended into the evidence array so the reader can see which
|
||||
// citations were verified.
|
||||
async function computeGrounding(findings: Finding[]): Promise<{ total: number; verified: number; rate: number }> {
|
||||
let verified = 0;
|
||||
for (const f of findings) {
|
||||
// readFile (async) instead of readFileSync — caught 2026-04-27 by
|
||||
// Kimi's self-audit. Sync I/O in an async fn blocks the event loop
|
||||
// for every cited file; doesn't matter at 10 findings, would matter
|
||||
// at 100+.
|
||||
const checks = await Promise.all(findings.map(async (f) => {
|
||||
const cite = f.evidence[0] ?? "";
|
||||
const m = /^(\S+?):(\d+)/.exec(cite);
|
||||
if (!m) continue;
|
||||
if (!m) return false;
|
||||
const [, relpath, lineStr] = m;
|
||||
const line = Number(lineStr);
|
||||
if (!line || !relpath) continue;
|
||||
if (!line || !relpath) return false;
|
||||
const abs = relpath.startsWith("/") ? relpath : resolve(REPO_ROOT, relpath);
|
||||
if (!existsSync(abs)) {
|
||||
f.evidence.push("[grounding: file not found]");
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const lines = readFileSync(abs, "utf8").split("\n");
|
||||
const lines = (await readFile(abs, "utf8")).split("\n");
|
||||
if (line < 1 || line > lines.length) {
|
||||
f.evidence.push(`[grounding: line ${line} > EOF (${lines.length})]`);
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
f.evidence.push(`[grounding: verified at ${relpath}:${line}]`);
|
||||
verified++;
|
||||
return true;
|
||||
} catch (e) {
|
||||
f.evidence.push(`[grounding: read failed: ${(e as Error).message.slice(0, 80)}]`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
const verified = checks.filter(Boolean).length;
|
||||
const total = findings.length;
|
||||
return { total, verified, rate: total === 0 ? 0 : verified / total };
|
||||
}
|
||||
|
||||
@ -64,11 +64,17 @@ pub async fn chat(
|
||||
// upstream API sees the bare model id (e.g. "kimi-for-coding").
|
||||
let model = req.model.strip_prefix("kimi/").unwrap_or(&req.model).to_string();
|
||||
|
||||
// Flatten content to a plain String. api.kimi.com is text-only on
|
||||
// the coding endpoint; the OpenAI multimodal array shape
|
||||
// ([{type:"text",text:"..."},{type:"image_url",...}]) returns 400.
|
||||
// Message::text() concats text-parts and drops non-text. Caught
|
||||
// 2026-04-27 by Kimi's self-audit (kimi.rs:137 — content as raw
|
||||
// serde_json::Value risked upstream rejection).
|
||||
let body = KimiChatBody {
|
||||
model: model.clone(),
|
||||
messages: req.messages.iter().map(|m| KimiMessage {
|
||||
role: m.role.clone(),
|
||||
content: m.content.clone(),
|
||||
content: serde_json::Value::String(m.text()),
|
||||
}).collect(),
|
||||
max_tokens: req.max_tokens.unwrap_or(800),
|
||||
temperature: req.temperature.unwrap_or(0.3),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user