osborn 0.9.145 → 0.9.147

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.
@@ -210,6 +210,12 @@ export declare class ClaudeLLM extends llm.LLM {
210
210
  setActiveQuery(q: any): void;
211
211
  /** Remove an active query (called from ClaudeLLMStream when query completes) */
212
212
  removeActiveQuery(q: any): void;
213
+ /**
214
+ * Stop a single dispatch flow by agent_id.
215
+ * Returns true if the agent was found and stopped; false if not found.
216
+ * Does NOT affect other active queries (use abortQuery() to kill all).
217
+ */
218
+ stopAgent(agentId: string): boolean;
213
219
  /** Whether a persistent session is alive and consuming messages */
214
220
  hasSession(): boolean;
215
221
  /**
@@ -12,6 +12,7 @@ import { EventEmitter } from 'events';
12
12
  import { saveSessionMetadata, getSessionWorkspace } from './config.js';
13
13
  import { statusManager } from './status-manager.js';
14
14
  import { getResearchSystemPrompt, getDirectModeResearchPrompt } from './prompts.js';
15
+ import { getIndexPath } from './summary-index.js';
15
16
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
16
17
  import { join, dirname } from 'node:path';
17
18
  import { fileURLToPath } from 'node:url';
@@ -555,6 +556,9 @@ export class ClaudeLLM extends llm.LLM {
555
556
  // Active queries — multiple can be running (SDK queues them internally).
556
557
  // We keep ALL references so interrupt() can stop whatever is currently executing.
557
558
  #activeQueries = new Set();
559
+ // Per-agent-id query map — allows targeted stop of a single dispatch flow.
560
+ // Strictly additive; abortQuery/interruptQuery still iterate #activeQueries (kill-all).
561
+ #activeQueriesById = new Map();
558
562
  // Dedup guard — prevents double-firing reviewer/gate if SubagentStop fires
559
563
  // more than once for the same agent_id (e.g. retry edge cases).
560
564
  #dispatchedFor = new Set();
@@ -888,6 +892,7 @@ export class ClaudeLLM extends llm.LLM {
888
892
  catch { }
889
893
  }
890
894
  this.#activeQueries.clear();
895
+ this.#activeQueriesById.clear();
891
896
  console.log('🛑 All queries aborted (Ctrl+C equivalent)');
892
897
  }
893
898
  /**
@@ -942,6 +947,23 @@ export class ClaudeLLM extends llm.LLM {
942
947
  removeActiveQuery(q) {
943
948
  this.#activeQueries.delete(q);
944
949
  }
950
+ /**
951
+ * Stop a single dispatch flow by agent_id.
952
+ * Returns true if the agent was found and stopped; false if not found.
953
+ * Does NOT affect other active queries (use abortQuery() to kill all).
954
+ */
955
+ stopAgent(agentId) {
956
+ const q = this.#activeQueriesById.get(agentId);
957
+ if (!q)
958
+ return false;
959
+ try {
960
+ q.return?.();
961
+ }
962
+ catch { }
963
+ this.#activeQueriesById.delete(agentId);
964
+ this.#activeQueries.delete(q);
965
+ return true;
966
+ }
945
967
  // ============================================================
946
968
  // PERSISTENT SESSION — V1 query() with AsyncIterable<SDKUserMessage>
947
969
  // Single subprocess per voice session. First chat() does JSONL cold
@@ -1133,9 +1155,13 @@ export class ClaudeLLM extends llm.LLM {
1133
1155
  return;
1134
1156
  this.#dispatchedFor.add(agentId);
1135
1157
  try {
1158
+ const idxPathReviewer = (this.#sessionId && this.#opts.workingDirectory)
1159
+ ? getIndexPath(this.#sessionId, this.#opts.workingDirectory)
1160
+ : null;
1136
1161
  const prompt = [
1137
1162
  'Use the reviewer sub-agent to review this writer output for correctness/spec-adherence/obvious bugs.',
1138
1163
  'End your reply with exactly `VERDICT: ACCEPT` or `VERDICT: REJECT`.',
1164
+ ...(idxPathReviewer ? [`The session index is at ${idxPathReviewer} — you MUST read it before reviewing.`] : []),
1139
1165
  '',
1140
1166
  '<writer_output>',
1141
1167
  writerOutput.slice(0, 8000),
@@ -1148,8 +1174,11 @@ export class ClaudeLLM extends llm.LLM {
1148
1174
  const reviewerOptions = {
1149
1175
  cwd: this.#opts.workingDirectory,
1150
1176
  permissionMode: 'default',
1177
+ systemPrompt: NAMED_AGENTS.reviewer.prompt,
1178
+ allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'],
1151
1179
  hooks: {
1152
- PreToolUse: [{
1180
+ PreToolUse: [
1181
+ {
1153
1182
  matcher: '.*',
1154
1183
  hooks: [async (input) => {
1155
1184
  const toolName = input?.tool_name || 'unknown';
@@ -1157,7 +1186,26 @@ export class ClaudeLLM extends llm.LLM {
1157
1186
  emitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: 'reviewer' });
1158
1187
  return {};
1159
1188
  }],
1160
- }],
1189
+ },
1190
+ {
1191
+ matcher: '.*',
1192
+ hooks: [async (input) => {
1193
+ const toolName = input?.tool_name || '';
1194
+ const toolInput = input?.tool_input;
1195
+ if ((toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') &&
1196
+ !/\.(md|markdown|mdx|txt|rst|adoc)$/i.test(String(toolInput?.file_path))) {
1197
+ return {
1198
+ hookSpecificOutput: {
1199
+ hookEventName: 'PreToolUse',
1200
+ permissionDecision: 'deny',
1201
+ },
1202
+ reason: 'reviewer may only write documentation files (.md/.markdown/.mdx/.txt/.rst/.adoc)',
1203
+ };
1204
+ }
1205
+ return {};
1206
+ }],
1207
+ },
1208
+ ],
1161
1209
  PostToolUse: [{
1162
1210
  matcher: '.*',
1163
1211
  hooks: [async (input) => {
@@ -1173,6 +1221,7 @@ export class ClaudeLLM extends llm.LLM {
1173
1221
  console.log(`[DISPATCH] spawning reviewer for agentId=${agentId.slice(0, 8)}`);
1174
1222
  const reviewerQuery = query({ prompt, options: reviewerOptions });
1175
1223
  this.#activeQueries.add(reviewerQuery);
1224
+ this.#activeQueriesById.set(agentId, reviewerQuery);
1176
1225
  let reviewerText = '';
1177
1226
  try {
1178
1227
  for await (const msg of reviewerQuery) {
@@ -1184,6 +1233,7 @@ export class ClaudeLLM extends llm.LLM {
1184
1233
  }
1185
1234
  finally {
1186
1235
  this.#activeQueries.delete(reviewerQuery);
1236
+ this.#activeQueriesById.delete(agentId);
1187
1237
  }
1188
1238
  const verdictMatch = reviewerText.match(/VERDICT:\s*(ACCEPT|REJECT)/i);
1189
1239
  const verdict = verdictMatch ? verdictMatch[1].toUpperCase() : null;
@@ -1211,10 +1261,14 @@ export class ClaudeLLM extends llm.LLM {
1211
1261
  return;
1212
1262
  this.#dispatchedFor.add(agentId);
1213
1263
  try {
1264
+ const idxPathGate = (this.#sessionId && this.#opts.workingDirectory)
1265
+ ? getIndexPath(this.#sessionId, this.#opts.workingDirectory)
1266
+ : null;
1214
1267
  const prompt = [
1215
1268
  'Use the reasoner sub-agent to vet this research output.',
1216
1269
  'Determine whether the research is sufficient to answer the original question.',
1217
1270
  'End your reply with exactly `GATE: PASS` or `GATE: NEEDS-MORE`.',
1271
+ ...(idxPathGate ? [`The session index is at ${idxPathGate} — you MUST read it before reviewing.`] : []),
1218
1272
  '',
1219
1273
  '<research_output>',
1220
1274
  researchOutput.slice(0, 8000),
@@ -1227,6 +1281,7 @@ export class ClaudeLLM extends llm.LLM {
1227
1281
  const gateOptions = {
1228
1282
  cwd: this.#opts.workingDirectory,
1229
1283
  permissionMode: 'default',
1284
+ systemPrompt: NAMED_AGENTS.reasoner.prompt,
1230
1285
  hooks: {
1231
1286
  PreToolUse: [{
1232
1287
  matcher: '.*',
@@ -1252,6 +1307,7 @@ export class ClaudeLLM extends llm.LLM {
1252
1307
  console.log(`[DISPATCH] spawning research-gate for agentId=${agentId.slice(0, 8)}`);
1253
1308
  const gateQuery = query({ prompt, options: gateOptions });
1254
1309
  this.#activeQueries.add(gateQuery);
1310
+ this.#activeQueriesById.set(agentId, gateQuery);
1255
1311
  let review = '';
1256
1312
  try {
1257
1313
  for await (const msg of gateQuery) {
@@ -1263,6 +1319,7 @@ export class ClaudeLLM extends llm.LLM {
1263
1319
  }
1264
1320
  finally {
1265
1321
  this.#activeQueries.delete(gateQuery);
1322
+ this.#activeQueriesById.delete(agentId);
1266
1323
  }
1267
1324
  const gateMatch = review.match(/GATE:\s*(PASS|NEEDS-MORE)/i);
1268
1325
  const gateVerdict = gateMatch ? gateMatch[1].toUpperCase() : null;
@@ -1749,6 +1806,7 @@ class ClaudeLLMStream extends llm.LLMStream {
1749
1806
  matcher: '.*',
1750
1807
  hooks: [async (input) => {
1751
1808
  console.log('[LIFECYCLE-PROBE] SubagentStart', JSON.stringify(input));
1809
+ this.#eventEmitter.emit('agent_started', { agent_type: input?.agent_type, agent_id: input?.agent_id });
1752
1810
  return {};
1753
1811
  }]
1754
1812
  }],
@@ -1760,6 +1818,7 @@ class ClaudeLLMStream extends llm.LLMStream {
1760
1818
  const msg = String(input?.last_assistant_message ?? '');
1761
1819
  const aid = input?.agent_id ?? ('sa-' + Date.now());
1762
1820
  statusManager.upsertDispatch(aid, { subagentType: at, dispatchState: 'completed', artifact: msg });
1821
+ this.#eventEmitter.emit('task_completed', { agent_type: at, agent_id: aid, last_assistant_message: String(msg).slice(0, 400) });
1763
1822
  // Infinite-loop guard — never re-dispatch the reviewer or reasoner.
1764
1823
  if (at === 'reviewer' || at === 'reasoner')
1765
1824
  return {};
package/dist/index.js CHANGED
@@ -3096,6 +3096,19 @@ async function main() {
3096
3096
  review: data.review,
3097
3097
  });
3098
3098
  });
3099
+ // Dispatcher v1 — sub-agent completed → frontend (Feature A)
3100
+ directLLM.events.on('task_completed', (d) => sendToFrontend({
3101
+ type: 'task_completed',
3102
+ agent_type: d.agent_type,
3103
+ agent_id: d.agent_id,
3104
+ last_assistant_message: d.last_assistant_message,
3105
+ }));
3106
+ // Dispatcher v1 — sub-agent started → frontend stop-key signal (Feature A2)
3107
+ directLLM.events.on('agent_started', (d) => sendToFrontend({
3108
+ type: 'agent_started',
3109
+ agent_type: d.agent_type,
3110
+ agent_id: d.agent_id,
3111
+ }));
3099
3112
  // Create the Agent with instructions, STT, LLM, TTS
3100
3113
  // VAD (Silero ONNX) removed — caused 2-5s inference lag on CPU, making interruption detection worse
3101
3114
  // Turn detection is server-side (Deepgram endpointing), interruptions handled by STT
@@ -5515,6 +5528,10 @@ async function main() {
5515
5528
  }
5516
5529
  }
5517
5530
  }
5531
+ // Feature B — per-flow stop (does NOT affect other running flows)
5532
+ else if (data.type === 'stop_dispatch' && currentLLM) {
5533
+ currentLLM.stopAgent?.(String(data.agentId));
5534
+ }
5518
5535
  else if (data.type === 'join_meeting') {
5519
5536
  const meetingUrl = data.url;
5520
5537
  if (meetingUrl) {
@@ -67,6 +67,7 @@ export declare class PipelineDirectLLM extends llm.LLM {
67
67
  abortAgent(): void;
68
68
  rewindAgent(checkpointId?: string): Promise<boolean>;
69
69
  hasActiveAgent(): boolean;
70
+ stopAgent(agentId: string): boolean;
70
71
  /** Send a new prompt to Claude via direct chat() — event listeners stay attached */
71
72
  sendPrompt(prompt: string): void;
72
73
  enableMcpServer(k: string, c: any): void;
@@ -120,6 +120,7 @@ export class PipelineDirectLLM extends llm.LLM {
120
120
  abortAgent() { this.#claudeLLM.abortQuery(); }
121
121
  async rewindAgent(checkpointId) { return this.#claudeLLM.rewindToCheckpoint(checkpointId); }
122
122
  hasActiveAgent() { return this.#claudeLLM.hasActiveQuery(); }
123
+ stopAgent(agentId) { return this.#claudeLLM.stopAgent(agentId); }
123
124
  /** Send a new prompt to Claude via direct chat() — event listeners stay attached */
124
125
  sendPrompt(prompt) {
125
126
  console.log(`📋 [pipeline] Sending prompt to Claude (${prompt.length} chars)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.145",
3
+ "version": "0.9.147",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {