kaizen-agent 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -305,7 +305,7 @@ the OS can find — Spotlight on macOS, the app menu on Linux, a Desktop shortcu
305
305
  edit, a reject reason, a note on a run — has a mic. Press it, talk, press it again; the
306
306
  words land at the caret next to whatever you had typed. Recognition is Whisper running
307
307
  inside the page, so audio never leaves your machine. The first press downloads the model
308
- from huggingface.co (about 165 MB on a WebGPU browser, 80 MB on the CPU fallback) and
308
+ from huggingface.co (about 165 MB on a WebGPU browser, up to 205 MB on a GPU without fp16, 80 MB on the CPU fallback) and
309
309
  caches it in the browser. The transformers.js library itself is fetched from jsDelivr the
310
310
  first time you press the mic in a session, so that first press needs the network; the
311
311
  model does not download again. Chromium and Edge qualify (Firefox falls back to the
package/cli/install.ts CHANGED
@@ -221,6 +221,7 @@ if (wantWeb) {
221
221
  // The pid file only knows a --daemon; a board started from the shortcut or a
222
222
  // terminal has none, so also ask the OS what listens on the port.
223
223
  const recorded = existsSync(pidFile) ? Number(readFileSync(pidFile, "utf8")) : NaN;
224
+ const { pidOnPort } = await import("./state.ts");
224
225
  const listening = await pidOnPort(port ?? 7420);
225
226
  const pids = [...new Set([recorded, listening].filter((p): p is number => Number.isFinite(p) && p > 0))];
226
227
  if (!pids.length) { console.log(" nothing running on the kaizen web port"); process.exit(0); }
@@ -797,19 +798,6 @@ if (blocked) process.exit(1);
797
798
 
798
799
  // ---------------------------------------------------------------- helpers
799
800
 
800
- // Whoever holds the board's port, by the OS's own accounting.
801
- async function pidOnPort(port: number): Promise<number | null> {
802
- try {
803
- if (process.platform === "win32") {
804
- const out = await Bun.$`netstat -ano -p tcp`.text();
805
- const m = new RegExp(`127\\.0\\.0\\.1:${port}\\s+\\S+\\s+LISTENING\\s+(\\d+)`).exec(out);
806
- return m ? Number(m[1]) : null;
807
- }
808
- const out = await Bun.$`lsof -t -iTCP:${port} -sTCP:LISTEN`.quiet().nothrow().text();
809
- return Number(out.trim().split("\n")[0]) || null;
810
- } catch { return null; }
811
- }
812
-
813
801
  function step(msg: string) { console.log(` ${c.green("+")} ${msg}`); }
814
802
 
815
803
  // Runs the upgrade and reports what it actually did, version to version, so the
package/cli/state.ts CHANGED
@@ -689,3 +689,16 @@ export function launchRun(it: Item, from: string): Launch {
689
689
  return { ok: false, agent, prompt, projectDir, manual, why: err?.message ?? String(err) };
690
690
  }
691
691
  }
692
+
693
+ // Whoever holds the board's port, by the OS's own accounting.
694
+ export async function pidOnPort(port: number): Promise<number | null> {
695
+ try {
696
+ if (process.platform === "win32") {
697
+ const out = await Bun.$`netstat -ano -p tcp`.text();
698
+ const m = new RegExp(`127\\.0\\.0\\.1:${port}\\s+\\S+\\s+LISTENING\\s+(\\d+)`).exec(out);
699
+ return m ? Number(m[1]) : null;
700
+ }
701
+ const out = await Bun.$`lsof -t -iTCP:${port} -sTCP:LISTEN`.quiet().nothrow().text();
702
+ return Number(out.trim().split("\n")[0]) || null;
703
+ } catch { return null; }
704
+ }
package/cli/web.ts CHANGED
@@ -5,7 +5,7 @@ import { join, dirname } from "node:path";
5
5
  import {
6
6
  home, type Item, boardCards, knownProjects, remember, locate, label, tilde,
7
7
  readInbox, writeInbox, replaceIdea, abandonRun, launchRun, parseItem, short, setArchived, writeNotes,
8
- appendNote, saveAttachment,
8
+ appendNote, saveAttachment, pidOnPort,
9
9
  } from "./state.ts";
10
10
 
11
11
  const PAGE = join(dirname(import.meta.dir), "web", "board.html");
@@ -108,6 +108,13 @@ export async function web(repoDir: string, opts: Opts = {}) {
108
108
  const url = `http://127.0.0.1:${port}/`;
109
109
  const up = await fetch(url, { signal: AbortSignal.timeout(1500) }).then((r) => r.ok, () => false);
110
110
  if (up) { console.log(`\n kaizen web already at ${url}\n`); if (opts.open !== false) await openApp(url); return; }
111
+ // A listener that does not answer is a board that hung; it is ours to stop.
112
+ const pid = await pidOnPort(port);
113
+ if (pid && pid !== process.pid) {
114
+ try { process.kill(pid, "SIGTERM"); console.log(` stopped a hung board on ${port} (pid ${pid})`); } catch { /* not ours to kill */ }
115
+ await Bun.sleep(300);
116
+ try { server = serve(port); continue; } catch { /* still held */ }
117
+ }
111
118
  console.log(` port ${port} refused by the OS and nothing answers there; trying ${port + 1}`);
112
119
  }
113
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "Staged workflow for AI agents — code, writing, research, operations: plan, human approval, build, independent review, bounded fix loop.",
5
5
  "type": "module",
6
6
  "bin": {
package/web/board.html CHANGED
@@ -229,7 +229,12 @@ const TFJS="https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0",ASR_MO
229
229
  // Inference lives in a module Worker so a slow chunk never freezes the board.
230
230
  const WORKER_SRC=`import{pipeline}from"${TFJS}";let asr=null;
231
231
  onmessage=async e=>{const m=e.data;try{
232
- if(m.type==="load"){asr=await pipeline("automatic-speech-recognition","${ASR_MODEL}",{device:m.webgpu?"webgpu":"wasm",dtype:m.webgpu?{encoder_model:"fp16",decoder_model_merged:"q4"}:"q8",progress_callback:p=>{if(p.status==="progress")postMessage({type:"progress",file:p.file,pct:p.progress||0})}});postMessage({type:"ready"})}
232
+ if(m.type==="load"){const prog=p=>{if(p.status==="progress")postMessage({type:"progress",file:p.file,pct:p.progress||0})};
233
+ // WebGPU first, with the encoder in fp16 only where the adapter has shader-f16; any GPU failure falls back to the CPU.
234
+ const gpu=m.webgpu?{device:"webgpu",dtype:{encoder_model:m.f16?"fp16":"fp32",decoder_model_merged:"q4"}}:null,cpu={device:"wasm",dtype:"q8"};
235
+ try{if(!gpu)throw 0;asr=await pipeline("automatic-speech-recognition","${ASR_MODEL}",{...gpu,progress_callback:prog})}
236
+ catch(e){if(gpu)postMessage({type:"fallback",why:String(e&&e.message||e)});asr=await pipeline("automatic-speech-recognition","${ASR_MODEL}",{...cpu,progress_callback:prog})}
237
+ postMessage({type:"ready"})}
233
238
  if(m.type==="run"){const r=await asr(m.audio,{chunk_length_s:30,return_timestamps:false});postMessage({type:"text",sid:m.sid,seq:m.seq,text:(r.text||"").trim()})}
234
239
  }catch(err){postMessage({type:"error",sid:m.sid,seq:m.seq,error:String(err&&err.message||err)})}}`;
235
240
  // The worklet only forwards 128-frame blocks; chunking and silence detection stay on the main thread.
@@ -238,11 +243,13 @@ const HAS_MIC=!!(navigator.mediaDevices&&navigator.mediaDevices.getUserMedia&&wi
238
243
  // `active` is the one session listening; `sessions` also holds stopped ones still waiting on a chunk.
239
244
  let asrWorker=null,asrReady=null,asrProgress=()=>{},active=null,starting=false;const sessions=new Set();
240
245
  // One worker for the page; loading is awaited by whoever pressed first, later presses join.
241
- function loadAsr(){if(asrReady)return asrReady;asrReady=new Promise((res,rej)=>{
246
+ function loadAsr(){if(asrReady)return asrReady;asrReady=new Promise(async(res,rej)=>{
242
247
  asrWorker=new Worker(URL.createObjectURL(new Blob([WORKER_SRC],{type:"text/javascript"})),{type:"module"});
243
- const files={};asrWorker.onmessage=e=>{const m=e.data;if(m.type==="progress"){files[m.file]=m.pct;const v=Object.values(files);asrProgress(Math.round(v.reduce((a,b)=>a+b,0)/v.length))}else if(m.type==="ready")res();else if(m.type==="error"&&m.seq==null)rej(new Error(m.error));for(const s of sessions)s.onmsg(m)};
248
+ const files={};asrWorker.onmessage=e=>{const m=e.data;if(m.type==="progress"){files[m.file]=m.pct;const v=Object.values(files);asrProgress(Math.round(v.reduce((a,b)=>a+b,0)/v.length))}else if(m.type==="ready")res();else if(m.type==="fallback")toast("GPU could not run the model, using the CPU: "+m.why);else if(m.type==="error"&&m.seq==null)rej(new Error(m.error));for(const s of sessions)s.onmsg(m)};
244
249
  asrWorker.onerror=e=>rej(new Error(e.message||"worker failed"));
245
- const webgpu=!!navigator.gpu;if(!webgpu)toast("No WebGPU: dictation will run on the CPU and be slower");asrWorker.postMessage({type:"load",webgpu})});
250
+ // Ask the adapter what it can do: no adapter means the CPU path; no shader-f16 means an fp32 encoder.
251
+ let adapter=null;try{adapter=navigator.gpu&&await navigator.gpu.requestAdapter()}catch(e){}
252
+ if(!adapter)toast("No WebGPU: dictation will run on the CPU and be slower");asrWorker.postMessage({type:"load",webgpu:!!adapter,f16:!!(adapter&&adapter.features.has("shader-f16"))})});
246
253
  asrReady.catch(()=>{asrReady=null;try{asrWorker.terminate()}catch(e){}asrWorker=null});return asrReady}
247
254
  // After a re-render only a listener whose field is gone is stopped; closing the panel stops any.
248
255
  function stopDictation(all){if(active&&(all||!active.field.isConnected))active.stop()}