osborn 0.9.244 → 0.9.246

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.
Files changed (2) hide show
  1. package/dist/claude-llm.js +69 -10
  2. package/package.json +1 -1
@@ -1002,25 +1002,63 @@ class MessageChannel {
1002
1002
  * refuses, caps, or blocks a dispatch — the agent reads it and decides. Returns
1003
1003
  * '' on any error so it can never break the turn.
1004
1004
  */
1005
- function buildMachineNudge() {
1005
+ /**
1006
+ * Instant CPU busy% from /proc/stat (Linux). One aggregate 'cpu' line: idle+iowait
1007
+ * vs total. Two reads a short interval apart give real-time utilization across all
1008
+ * cores — unlike the 1-min loadavg, which lags a fresh burst of heavy commands by
1009
+ * ~30-60s. Returns null off-Linux / on any parse error (caller falls back to loadavg).
1010
+ */
1011
+ function readProcStatCpu() {
1012
+ try {
1013
+ const line = readFileSync('/proc/stat', 'utf8').split('\n', 1)[0];
1014
+ const n = line.trim().split(/\s+/).slice(1).map(Number); // drop the "cpu" label
1015
+ if (n.length < 5 || n.some(Number.isNaN))
1016
+ return null;
1017
+ const idle = n[3] + (n[4] || 0); // idle + iowait
1018
+ const total = n.reduce((a, b) => a + b, 0);
1019
+ return { idle, total };
1020
+ }
1021
+ catch {
1022
+ return null;
1023
+ }
1024
+ }
1025
+ async function buildMachineNudge() {
1006
1026
  try {
1007
1027
  const ncpu = Math.max(1, cpus().length);
1008
- const cpuPct = Math.round((loadavg()[0] / ncpu) * 100); // 1-min load ÷ cores
1028
+ // #3: prefer an INSTANT CPU read (proc/stat delta over ~200ms) over the lagging
1029
+ // 1-min loadavg, so a fresh storm of tsc/build/test shows up on THIS turn, not a
1030
+ // minute later. Falls back to loadavg÷cores off-Linux or if the delta is unusable.
1031
+ let cpuPct;
1032
+ const a = readProcStatCpu();
1033
+ if (a) {
1034
+ await new Promise((r) => setTimeout(r, 200));
1035
+ const b = readProcStatCpu();
1036
+ const dTotal = b ? b.total - a.total : 0;
1037
+ cpuPct = b && dTotal > 0
1038
+ ? Math.round(((dTotal - (b.idle - a.idle)) / dTotal) * 100)
1039
+ : Math.round((loadavg()[0] / ncpu) * 100);
1040
+ }
1041
+ else {
1042
+ cpuPct = Math.round((loadavg()[0] / ncpu) * 100);
1043
+ }
1009
1044
  const memPct = Math.round((1 - freemem() / totalmem()) * 100);
1010
1045
  const subagents = statusManager.getActiveDispatchIds().length;
1011
- // Recommended concurrent heavy sub-agents, adapted to live headroom. This is a
1012
- // concrete DEFAULT the agent should follow unless a task genuinely warrants
1013
- // otherwise — surfaced as an actual number/target, never enforced.
1046
+ // Recommended concurrent heavy sub-agents, adapted to live headroom. A concrete
1047
+ // DEFAULT the agent follows unless a task warrants otherwise — never enforced.
1014
1048
  const recMax = (cpuPct >= 80 || memPct >= 90) ? Math.max(1, ncpu - 1) // hot: pull back below cores
1015
1049
  : (cpuPct >= 60) ? ncpu // busy: ~cores
1016
1050
  : ncpu + 1; // calm: a little headroom
1017
1051
  const over = subagents > recMax;
1018
1052
  const warn = over || cpuPct >= 80;
1019
- const head = `[MACHINE${warn ? ' ⚠️' : ''}] CPU ~${cpuPct}% · MEM ${memPct}% · sub-agents ${subagents}/${recMax} recommended`;
1053
+ // #4: name the actual CPU culprits — the spikes come from heavy COMMANDS sub-agents
1054
+ // run, not their count. One tsc/build/test pegs a core for 30-120s; ten file-reading
1055
+ // agents barely register. Tell the agent WHAT to throttle.
1056
+ const culprits = 'CPU cost is heavy commands (tsc/typecheck, build, test, npm/pnpm install, eslint) — not agent count. Prefer scoped/incremental runs, cache, and never fire several at once.';
1057
+ const head = `[MACHINE${warn ? ' ⚠️' : ''}] CPU ${cpuPct}% · MEM ${memPct}% · sub-agents ${subagents}/${recMax} recommended`;
1020
1058
  if (over)
1021
- return `${head} — over the recommended concurrency; wind down to ≤${recMax} (finish or stop in-flight sub-agents) unless a task must continue. (recommendation — the agent decides, not enforced)`;
1059
+ return `${head} — over the recommended concurrency; wind down to ≤${recMax} (finish or stop in-flight sub-agents) unless a task must continue. ${culprits} (recommendation — the agent decides, not enforced)`;
1022
1060
  if (cpuPct >= 80)
1023
- return `${head} — load high; hold new sub-agents beyond ${recMax} unless needed. (recommendation — the agent decides, not enforced)`;
1061
+ return `${head} — load high; hold new sub-agents beyond ${recMax} unless needed. ${culprits} (recommendation — the agent decides, not enforced)`;
1024
1062
  return head;
1025
1063
  }
1026
1064
  catch {
@@ -1779,6 +1817,10 @@ class ClaudeLLMStream extends llm.LLMStream {
1779
1817
  #abortController;
1780
1818
  #llmRef;
1781
1819
  #approvedWriterToolUseIds = new Set();
1820
+ // Last Task/Agent dispatch description per subagent_type, captured at PreToolUse so
1821
+ // SubagentStart can label the Running row with WHAT the sub-agent is doing even if the
1822
+ // SDK hook input omits the prompt. Cleared when consumed by the matching start.
1823
+ #pendingTaskByType = new Map();
1782
1824
  constructor(llmInstance, { chatCtx, toolCtx, connOptions, opts, sessionId, onSessionId, eventEmitter, onCheckpoint, onPermissionRequest, abortController, }) {
1783
1825
  super(llmInstance, { chatCtx, toolCtx, connOptions });
1784
1826
  this.#llmRef = llmInstance;
@@ -1988,6 +2030,14 @@ class ClaudeLLMStream extends llm.LLMStream {
1988
2030
  // tool_use, returns {} → canUseTool workspace auto-approve).
1989
2031
  }
1990
2032
  console.log(`🔧 Claude: ${toolName}`);
2033
+ // Capture the dispatch description/prompt per subagent_type so SubagentStart
2034
+ // can show WHAT a sub-agent is working on (see #pendingTaskByType).
2035
+ if (toolName === 'Task' || toolName === 'Agent') {
2036
+ const st = String(toolInput?.subagent_type || toolInput?.agent_type || '');
2037
+ const d = String(toolInput?.description || toolInput?.prompt || '');
2038
+ if (st && d)
2039
+ this.#pendingTaskByType.set(st, d);
2040
+ }
1991
2041
  this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
1992
2042
  return {};
1993
2043
  }]
@@ -2064,7 +2114,7 @@ class ClaudeLLMStream extends llm.LLMStream {
2064
2114
  // Lightweight machine-load nudge (main agent only). Cheap synchronous
2065
2115
  // snapshot, prepended so it's present even when recall degrades under load.
2066
2116
  // Advisory only — never caps/refuses a dispatch.
2067
- const machineNudge = fromSubagent ? '' : buildMachineNudge();
2117
+ const machineNudge = fromSubagent ? '' : await buildMachineNudge();
2068
2118
  const additionalContext = [machineNudge, reminder, recalled].filter(Boolean).join('\n\n');
2069
2119
  console.log(`📌 UserPromptSubmit: injected turn-shape reminder (${reminder.length} chars)${recalled ? ` + recall (${recalled.length} chars)` : ''}${machineNudge ? ` + machine(${machineNudge.includes('⚠️') ? 'HOT' : 'ok'})` : ''} for prompt="${promptPreview}..." [tool budget reset to 0/${TOOL_CALL_BUDGET}]`);
2070
2120
  return {
@@ -2357,7 +2407,16 @@ class ClaudeLLMStream extends llm.LLMStream {
2357
2407
  matcher: '.*',
2358
2408
  hooks: [async (input) => {
2359
2409
  console.log('[LIFECYCLE-PROBE] SubagentStart', JSON.stringify(input));
2360
- this.#eventEmitter.emit('agent_started', { agent_type: input?.agent_type, agent_id: input?.agent_id });
2410
+ // Surface WHAT the sub-agent was dispatched to do (not just its type) so the
2411
+ // Running panel shows the task while it's in flight, before any result lands.
2412
+ // The SDK hook may carry the initial prompt/description; fall back to the last
2413
+ // Task/Agent dispatch of this type captured at PreToolUse. Best-effort.
2414
+ const at0 = input?.agent_type;
2415
+ const rawTask = input?.prompt ?? input?.description ?? (at0 ? this.#pendingTaskByType.get(String(at0)) : undefined);
2416
+ if (at0)
2417
+ this.#pendingTaskByType.delete(String(at0));
2418
+ const task = rawTask ? String(rawTask).replace(/\s+/g, ' ').trim().slice(0, 160) : undefined;
2419
+ this.#eventEmitter.emit('agent_started', { agent_type: input?.agent_type, agent_id: input?.agent_id, task });
2361
2420
  // Record the dispatch as LIVE so the frontend can reconcile orphaned
2362
2421
  // "running" rows against statusManager.getActiveDispatchIds(). Without a
2363
2422
  // start-time entry the manager only knew about dispatches at completion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.244",
3
+ "version": "0.9.246",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {