osborn 0.9.141 → 0.9.143

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.
@@ -10,6 +10,7 @@ import { llm, shortuuid, DEFAULT_API_CONNECT_OPTIONS } from '@livekit/agents';
10
10
  import { query } from '@anthropic-ai/claude-agent-sdk';
11
11
  import { EventEmitter } from 'events';
12
12
  import { saveSessionMetadata, getSessionWorkspace } from './config.js';
13
+ import { statusManager } from './status-manager.js';
13
14
  import { getResearchSystemPrompt, getDirectModeResearchPrompt } from './prompts.js';
14
15
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
15
16
  import { join, dirname } from 'node:path';
@@ -217,29 +218,41 @@ export const NAMED_AGENTS = {
217
218
  tools: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch'],
218
219
  model: 'opus',
219
220
  prompt: [
220
- 'You are Osborn\'s reasoning agent. Your job is deep analysis, architectural thinking, and decision-making.',
221
+ 'You are Osborn\'s reasoning agent — the "smart model" seat for hard tradeoffs, architecture decisions, and vetting research.',
221
222
  '',
222
223
  '## Your role',
224
+ 'You DECIDE. You do not route work — that is the orchestrator\'s job. You receive structured summaries (not raw dumps) and return clear, opinionated decisions with full rationale.',
223
225
  'Think hard about complex problems. Consider multiple approaches. Identify risks and edge cases.',
224
- 'Return a clear, opinionated recommendation with reasoning — not just a list of options.',
226
+ '',
227
+ '## Session context',
228
+ 'The orchestrator provides, as an artifact in your brief, the PATH to the session index file (search-index.txt — the running index of this session/mission). You MUST actually READ it — do not rely on a summary or a preloaded window. Read the full index (or the portions you need) directly to ground your analysis, understand the mission, and refine/manage the researchers\' work. Reach into the full index whenever a decision or a research review needs the fuller history — that direct reading is what sets your judgment apart.',
225
229
  '',
226
230
  '## How to work',
227
231
  '1. Read and understand the full context before forming an opinion.',
228
232
  '2. If the main agent provided researcher findings, use them as your starting point.',
229
- '3. Consider at least 2-3 alternative approaches before recommending one.',
230
- '4. Think about: correctness, maintainability, performance, failure modes, migration path.',
233
+ '3. Enumerate at least 2-3 alternative approaches before recommending one.',
234
+ '4. For each option consider: pros, cons, risks, and reversibility.',
231
235
  '5. Use Read/Grep to verify assumptions against the actual codebase when relevant.',
236
+ '6. Think about: correctness, maintainability, performance, failure modes, migration path.',
232
237
  '',
233
- '## What to return',
234
- '- RECOMMENDATION: what to do (one clear answer, not "it depends")',
235
- '- REASONING: why this approach wins over alternatives (2-3 sentences)',
238
+ '## Decision output format',
239
+ 'For a decision task, structure your response as:',
240
+ '- OPTIONS: for each option — pros / cons / risks / reversibility',
241
+ '- RECOMMENDATION: the chosen option (one clear answer, not "it depends")',
242
+ '- RATIONALE: one paragraph — why this option wins and what assumptions you are making',
236
243
  '- PLAN: step-by-step implementation instructions specific enough for the writer agent',
237
244
  '- RISKS: what could go wrong and how to mitigate',
238
- '- If the problem is genuinely ambiguous, say what additional information would resolve it',
245
+ '',
246
+ '## Research-review gate',
247
+ 'When reviewing a researcher\'s findings, judge whether the research is COMPLETE and well-sourced against the original task. If it is, pass it. If it\'s thin, missing sources, or the answer likely lives somewhere the researcher didn\'t look, report that it needs more — so the orchestrator sends the researcher back.',
248
+ 'End every research review with exactly one of:',
249
+ ' GATE: PASS',
250
+ ' GATE: NEEDS-MORE — <what\'s missing / where to look>',
239
251
  '',
240
252
  '## What NOT to do',
241
253
  '- Do NOT edit or write files — return a plan for the writer agent',
242
254
  '- Do NOT give wishy-washy "both options are valid" non-answers — commit to a recommendation',
255
+ '- Do NOT consume raw session dumps; ask the orchestrator for a structured summary instead',
243
256
  '- If you need more information, ask the main agent to delegate to the researcher',
244
257
  '',
245
258
  '## When to use / handoff',
@@ -312,23 +325,35 @@ export const NAMED_AGENTS = {
312
325
  'Execute test suites, build commands, and linters. Interpret failures clearly.',
313
326
  'You are a quality gate — find out whether the code works, and say exactly what broke.',
314
327
  '',
328
+ '## Backward-compatibility / regression mandate (CRITICAL)',
329
+ 'Existing test suites MUST still pass — any pre-existing test that breaks is a BLOCKER; report it as such.',
330
+ 'The public API surface (function signatures, exported types, return shapes, behavior) must NOT silently change.',
331
+ 'Flag any change that could break existing callers, even if no test currently covers it.',
332
+ '',
315
333
  '## How to work',
316
334
  '1. Identify the correct test / build command from package.json, Makefile, or the task brief.',
317
- '2. Run it with Bash. Capture stdout + stderr in full.',
318
- '3. If a command fails, read the relevant source files to locate the root cause.',
319
- '4. Cap yourself at 6-8 tool calls unless the investigation clearly requires more.',
335
+ '2. Run the FULL existing test suite first to establish the regression baseline.',
336
+ '3. Generate and run tests targeted at the SPECIFIC diff/change provided — at both unit and integration levels where relevant.',
337
+ '4. Exercise edge cases: boundary values, empty inputs, error/exception paths, null/undefined.',
338
+ '5. Execution loop: write test → run it → read failure output → fix the test OR flag as a real bug in the code. Do NOT silently paper over a real defect.',
339
+ '6. If a command fails, read the relevant source files to locate the root cause.',
340
+ '7. Cap yourself at 6-8 tool calls unless the investigation clearly requires more.',
320
341
  '',
321
342
  '## What to return',
322
343
  '- RESULT: PASS or FAIL (one word, first line)',
323
- '- COMMAND: the exact command you ran',
344
+ '- COMMAND: the exact command(s) you ran',
324
345
  '- OUTPUT: relevant excerpt (errors, failing test names, line numbers)',
325
346
  '- ROOT CAUSE: your diagnosis of why it failed (if applicable)',
347
+ '- TEST FILES: path(s) to any test files written or modified',
348
+ '- COVERAGE DELTA: what the change adds or leaves uncovered (before vs after where determinable); list notable uncovered lines/paths',
349
+ '- REGRESSIONS / COMPAT BREAKS: explicit list of any pre-existing tests that now fail or API changes that could break existing callers — tag each as BLOCKER',
326
350
  '- What you checked but found to be unrelated',
327
351
  '',
328
352
  '## What NOT to do',
329
- '- Do NOT edit or write files — report failures so the writer agent can fix them',
353
+ '- Do NOT edit or write production files — report failures so the writer agent can fix them',
330
354
  '- Do NOT run destructive commands (no rm, no git push, no npm publish)',
331
355
  '- Do NOT guess at fixes — diagnose only',
356
+ '- Do NOT paper over a real defect by weakening or skipping a test',
332
357
  '',
333
358
  '## When to use / handoff',
334
359
  'Invoked in PARALLEL with reviewer, immediately after the writer returns a change.',
@@ -381,53 +406,78 @@ export const NAMED_AGENTS = {
381
406
  description: [
382
407
  'Code-review agent (Opus). Use for: the VERIFY step in a generator-verifier loop — after the',
383
408
  'writer completes a change, the reviewer reads the diff, checks correctness, spec/requirement',
384
- 'adherence, obvious bugs, and security issues, then returns an ACCEPT or REJECT verdict with',
385
- 'specific, actionable feedback. Does NOT edit files — reports so the writer can fix.',
409
+ 'adherence, obvious bugs, and security issues, then tags each finding BLOCKER/MAJOR/MINOR/NIT',
410
+ 'and returns an ACCEPT or REJECT verdict with specific, actionable feedback. May write documentation files (.md etc.) only.',
386
411
  ].join(' '),
387
- tools: ['Read', 'Glob', 'Grep', 'Bash'],
412
+ tools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'],
388
413
  model: 'opus',
389
414
  prompt: [
390
415
  'You are Osborn\'s reviewer agent. You are the VERIFY step in a generator-verifier loop.',
391
416
  '',
392
417
  '## Your role',
393
418
  'Read the writer\'s completed change (via git diff or by reading modified files), then produce',
394
- 'a structured verdict: ACCEPT or REJECT. You do NOT edit files — you report findings so the',
395
- 'single writer agent can fix them. You are the quality gate between a change and merge.',
419
+ 'a structured verdict: ACCEPT or REJECT. You report findings so the single writer agent can fix code.',
420
+ 'You may write documentation files (.md/.txt/etc.) only. You are the quality gate between a change and merge.',
396
421
  '',
397
422
  '## Bash is read-only inspection only',
398
423
  'You may run: git diff, git log, git status, git show, npm run build, npm test, eslint,',
399
424
  'tsc --noEmit, and similar lint/test/security-scan commands.',
400
425
  'You must NOT run: rm, git push, git commit, git add, npm publish, or any destructive command.',
401
426
  '',
427
+ '## Step 0 — Discover and adopt project standards (before reviewing)',
428
+ 'Look for existing project standards, conventions, and documentation: CLAUDE.md, AGENTS.md,',
429
+ 'docs/, README, style guides, and any gotchas/anti-pattern/decision notes in the repo or',
430
+ 'session memory. If present, ADOPT them as the standard you review against — check the change',
431
+ 'against these project-specific conventions and known past gotchas, not just generic best-practice.',
432
+ 'If NO such files exist, note that in your report and fall back to the task spec + general best-practice.',
433
+ '',
434
+ '## Maintain documentation',
435
+ 'You MAY create and maintain project standards and conventions files. Specifically: adopt an existing',
436
+ 'standards doc if one is found, or create one (e.g. CONVENTIONS.md, docs/standards.md) when none exists',
437
+ 'and the review reveals patterns worth recording. Keep documentation current as you review.',
438
+ 'RESTRICTION: you may ONLY write files with documentation extensions: .md, .markdown, .mdx, .txt, .rst, .adoc.',
439
+ 'You must NEVER write code, config, or source files (.ts, .js, .json, .env, etc.) — the write gate',
440
+ 'enforces this at the system level and will deny any such attempt.',
441
+ '',
402
442
  '## How to work',
403
443
  '1. Run `git diff` (or read the files listed in the task) to see exactly what changed.',
404
444
  '2. Read any file that needs context to evaluate the diff (interfaces, callers, tests).',
405
445
  '3. Run the build or test suite if available to catch compile/runtime regressions.',
406
- '4. Check against the spec or requirement provided in the task brief.',
446
+ '4. Check against the spec or requirement provided in the task brief AND any project standards found above.',
407
447
  '5. Look for: logic errors, missing edge cases, security issues (injection, path traversal,',
408
448
  ' credential exposure), broken types, spec deviations, unintended side-effects.',
409
449
  '6. Cap yourself at 10 tool calls unless the review clearly requires more.',
410
450
  '',
451
+ '## Severity taxonomy',
452
+ 'Tag EVERY finding with exactly one of:',
453
+ ' BLOCKER — incorrect behavior, data loss, security hole, broken build; must fix before merge',
454
+ ' MAJOR — significant bug or spec deviation that will likely cause real problems in use',
455
+ ' MINOR — non-critical defect or missed edge case worth fixing but not blocking',
456
+ ' NIT — style, naming, or polish; never a reason to REJECT on its own',
457
+ '',
411
458
  '## What to return',
412
459
  'Structure your response EXACTLY as follows:',
413
460
  '',
414
- 'VERDICT: ACCEPT | REJECT',
461
+ 'VERDICT: ACCEPT | REJECT — <one-line rationale>',
415
462
  '',
416
- 'ISSUES (each on its own line, only present if VERDICT is REJECT):',
417
- ' - <file>:<line> — <concise description of the problem and why it matters>',
463
+ 'ISSUES (omit section entirely if VERDICT is ACCEPT):',
464
+ ' [SEVERITY] <file>:<line>',
465
+ ' Evidence: "<short quote of the offending code>"',
466
+ ' Impact: <what breaks or why it matters>',
467
+ ' Fix: <recommended change>',
468
+ ' Verify: <how to confirm the fix is correct>',
418
469
  '',
419
470
  'WHAT IT CHECKED-AND-CLEARED:',
420
471
  ' - <each item you verified and found correct — be specific, not generic>',
421
472
  '',
422
473
  '## What NOT to do',
423
- '- Do NOT edit or write any files — issue reports only; the writer fixes',
474
+ '- Do NOT write code, config, or source files — only documentation-extension files (.md/.markdown/.mdx/.txt/.rst/.adoc) are permitted',
424
475
  '- Do NOT run destructive commands (no rm, no git push, no git commit, no npm publish)',
425
476
  '- Do NOT approve a change that has a real defect just to be agreeable',
426
- '- Do NOT raise trivial style nits as REJECT-worthy issues unless they break functionality',
477
+ '- Do NOT REJECT solely on NIT-level findings',
427
478
  '',
428
479
  '## When to use / handoff',
429
480
  'Invoked in PARALLEL with tester, immediately after the writer returns a change.',
430
- 'Return an ACCEPT or REJECT verdict with specific, actionable issue reports.',
431
481
  'The orchestrator waits for both reviewer and tester before synthesizing and speaking to the user.',
432
482
  ].join('\n'),
433
483
  },
@@ -505,6 +555,9 @@ export class ClaudeLLM extends llm.LLM {
505
555
  // Active queries — multiple can be running (SDK queues them internally).
506
556
  // We keep ALL references so interrupt() can stop whatever is currently executing.
507
557
  #activeQueries = new Set();
558
+ // Dispatcher v1 — maps tool_use_id → subagent_type for Task tool_use blocks
559
+ // so that when task_summary fires we know which agent type just finished.
560
+ #dispatchAgentTypes = new Map();
508
561
  constructor(opts = {}) {
509
562
  super();
510
563
  // Session resume/continue options
@@ -1039,6 +1092,29 @@ export class ClaudeLLM extends llm.LLM {
1039
1092
  callbacks.eventEmitter.emit('tts_say', { text: ttsChunk });
1040
1093
  }
1041
1094
  }
1095
+ // Dispatcher v1 — record Task tool_use blocks so we can correlate
1096
+ // the matching task_summary message to the subagent type.
1097
+ if (block.type === 'tool_use' && block.name === 'Task') {
1098
+ console.log('[DISPATCH-PROBE] task tool_use', JSON.stringify({ id: block.id, subagent_type: block.input?.subagent_type }));
1099
+ this.#dispatchAgentTypes.set(block.id, block.input?.subagent_type);
1100
+ statusManager.upsertDispatch(block.id, {
1101
+ owner: 'orchestrator',
1102
+ subagentType: block.input?.subagent_type,
1103
+ dispatchState: 'running',
1104
+ });
1105
+ }
1106
+ }
1107
+ }
1108
+ // Dispatcher v1 — catch sub-agent completion signals
1109
+ if (msg.type === 'system' && msg.subtype === 'task_summary') {
1110
+ console.log('[DISPATCH-PROBE] task_summary raw:', JSON.stringify(msg).slice(0, 500));
1111
+ const tuid = msg.tool_use_id ?? msg.toolUseId;
1112
+ const output = msg.summary ?? msg.result ?? msg.output ?? '';
1113
+ const subType = tuid ? this.#dispatchAgentTypes.get(tuid) : undefined;
1114
+ statusManager.upsertDispatch(tuid ?? `ts-${Date.now()}`, { subagentType: subType, dispatchState: 'completed', artifact: String(output) });
1115
+ console.log(`[DISPATCH] completed type=${subType ?? '?'} tuid=${(tuid ?? '?').slice(0, 8)} len=${String(output).length}`);
1116
+ if (subType === 'writer' && output) {
1117
+ this.#spawnReviewer(tuid, String(output), callbacks.eventEmitter);
1042
1118
  }
1043
1119
  }
1044
1120
  // Result — marks end of a turn (but we keep consuming for next turn)
@@ -1067,6 +1143,56 @@ export class ClaudeLLM extends llm.LLM {
1067
1143
  console.log('🔒 Persistent session background consumer exited');
1068
1144
  }
1069
1145
  }
1146
+ /**
1147
+ * Dispatcher v1 — auto-spawn a reviewer after a writer sub-agent completes.
1148
+ * Runs a one-shot query() with the reviewer agent and emits dispatch_rejected
1149
+ * if the verdict is REJECT. A reviewer failure must never crash the consumer.
1150
+ */
1151
+ async #spawnReviewer(tuid, writerOutput, emitter) {
1152
+ try {
1153
+ const prompt = [
1154
+ 'Use the reviewer sub-agent to review this writer output for correctness/spec-adherence/obvious bugs.',
1155
+ 'End your reply with exactly `VERDICT: ACCEPT` or `VERDICT: REJECT`.',
1156
+ '',
1157
+ '<writer_output>',
1158
+ writerOutput.slice(0, 8000),
1159
+ '</writer_output>',
1160
+ ].join('\n');
1161
+ const reviewerOptions = {
1162
+ cwd: this.#opts.workingDirectory,
1163
+ permissionMode: 'default',
1164
+ agents: NAMED_AGENTS,
1165
+ };
1166
+ console.log(`[DISPATCH] spawning reviewer for tuid=${tuid.slice(0, 8)}`);
1167
+ const reviewerQuery = query({ prompt, options: reviewerOptions });
1168
+ this.#activeQueries.add(reviewerQuery);
1169
+ let reviewerText = '';
1170
+ try {
1171
+ for await (const msg of reviewerQuery) {
1172
+ const m = msg;
1173
+ if (m.type === 'result' && m.result) {
1174
+ reviewerText = String(m.result);
1175
+ }
1176
+ }
1177
+ }
1178
+ finally {
1179
+ this.#activeQueries.delete(reviewerQuery);
1180
+ }
1181
+ const verdictMatch = reviewerText.match(/VERDICT:\s*(ACCEPT|REJECT)/i);
1182
+ const verdict = verdictMatch ? verdictMatch[1].toUpperCase() : null;
1183
+ if (verdict === 'REJECT') {
1184
+ console.log(`[DISPATCH] review REJECT for tuid=${tuid.slice(0, 8)}`);
1185
+ statusManager.upsertDispatch(tuid, { dispatchState: 'rejected', artifact: reviewerText });
1186
+ emitter.emit('dispatch_rejected', { tuid, verdict: 'REJECT', review: reviewerText });
1187
+ }
1188
+ else {
1189
+ console.log(`[DISPATCH] review ACCEPT for tuid=${tuid.slice(0, 8)}`);
1190
+ }
1191
+ }
1192
+ catch (err) {
1193
+ console.error('[DISPATCH] reviewer spawn failed (non-fatal):', err);
1194
+ }
1195
+ }
1070
1196
  chat({ chatCtx, toolCtx, connOptions = DEFAULT_API_CONNECT_OPTIONS, abortController, }) {
1071
1197
  return new ClaudeLLMStream(this, {
1072
1198
  chatCtx,
@@ -1285,6 +1411,21 @@ class ClaudeLLMStream extends llm.LLMStream {
1285
1411
  this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
1286
1412
  return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
1287
1413
  }
1414
+ // Reviewer agent: ONLY documentation-extension files allowed — fail closed
1415
+ if (agentType === 'reviewer') {
1416
+ const reviewerPath = String(toolInput.file_path || '');
1417
+ const DOC_EXTENSIONS = /\.(md|markdown|mdx|txt|rst|adoc)$/i;
1418
+ if (!reviewerPath || !DOC_EXTENSIONS.test(reviewerPath)) {
1419
+ const reason = reviewerPath
1420
+ ? `Reviewer write denied: ${reviewerPath} is not a documentation file (.md/.markdown/.mdx/.txt/.rst/.adoc). Reviewer may only write documentation.`
1421
+ : 'Reviewer write denied: could not determine target file path. Failing closed.';
1422
+ console.log(`🚫 Reviewer write blocked: ${reviewerPath || '(no path)'} — not a doc extension`);
1423
+ this.#eventEmitter.emit('tool_blocked', { name: toolName, reason });
1424
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason };
1425
+ }
1426
+ console.log(`📝 Reviewer doc write allowed: ${reviewerPath}`);
1427
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
1428
+ }
1288
1429
  // All other agents (main, researcher, reasoner, etc.): workspace only
1289
1430
  const filePath = String(toolInput.file_path || '');
1290
1431
  if (filePath && !filePath.includes('/osb/') && !filePath.includes('.osborn/sessions/') && !filePath.includes('.osborn/research/')) {
package/dist/index.js CHANGED
@@ -3086,6 +3086,16 @@ async function main() {
3086
3086
  checkpointId: data.checkpointId,
3087
3087
  });
3088
3088
  });
3089
+ // Dispatcher v1 — writer reviewer rejected verdict → frontend
3090
+ directLLM.events.on('dispatch_rejected', (data) => {
3091
+ console.log(`[DISPATCH] dispatch_rejected tuid=${(data.tuid ?? '').slice(0, 8)} sending to frontend`);
3092
+ sendToFrontend({
3093
+ type: 'task_review',
3094
+ tuid: data.tuid,
3095
+ verdict: data.verdict,
3096
+ review: data.review,
3097
+ });
3098
+ });
3089
3099
  // Create the Agent with instructions, STT, LLM, TTS
3090
3100
  // VAD (Silero ONNX) removed — caused 2-5s inference lag on CPU, making interruption detection worse
3091
3101
  // Turn detection is server-side (Deepgram endpointing), interruptions handled by STT
@@ -25,7 +25,7 @@ function buildSessionTail(sessionId, workingDir) {
25
25
  return '';
26
26
  if (!sessionId)
27
27
  return '';
28
- const maxLines = parseInt(process.env.OSBORN_SESSION_TAIL_COUNT || '2000', 10);
28
+ const maxLines = parseInt(process.env.OSBORN_SESSION_TAIL_COUNT || '3000', 10);
29
29
  try {
30
30
  const indexPath = getIndexPath(sessionId, workingDir);
31
31
  if (!indexPath)
@@ -18,6 +18,10 @@ export interface TaskStatus {
18
18
  progress?: number;
19
19
  progressUpdates: string[];
20
20
  lastUpdate?: string;
21
+ owner?: string;
22
+ subagentType?: string;
23
+ dispatchState?: 'pending' | 'running' | 'completed' | 'rejected' | 'failed';
24
+ artifact?: string;
21
25
  }
22
26
  export interface StatusUpdate {
23
27
  hasUpdates: boolean;
@@ -86,5 +90,16 @@ export declare class StatusManager {
86
90
  * Get context summary for the brain
87
91
  */
88
92
  getContextSummary(): string;
93
+ /**
94
+ * Dispatcher v1 — create or update a dispatch entry keyed by tool_use_id.
95
+ * Creates a minimal TaskStatus shell if the id doesn't yet exist so callers
96
+ * can upsert without a prior registerTask() call.
97
+ */
98
+ upsertDispatch(id: string, fields: {
99
+ owner?: string;
100
+ subagentType?: string;
101
+ dispatchState?: TaskStatus['dispatchState'];
102
+ artifact?: string;
103
+ }): void;
89
104
  }
90
105
  export declare const statusManager: StatusManager;
@@ -182,6 +182,48 @@ export class StatusManager {
182
182
  getContextSummary() {
183
183
  return this.conversationContext.slice(-5).join(' | ');
184
184
  }
185
+ /**
186
+ * Dispatcher v1 — create or update a dispatch entry keyed by tool_use_id.
187
+ * Creates a minimal TaskStatus shell if the id doesn't yet exist so callers
188
+ * can upsert without a prior registerTask() call.
189
+ */
190
+ upsertDispatch(id, fields) {
191
+ if (!this.tasks.has(id)) {
192
+ this.tasks.set(id, {
193
+ id,
194
+ type: 'execute',
195
+ // Use an empty query so narration never speaks a bogus "dispatch:<id>" string.
196
+ // Dispatch progress is tracked exclusively via dispatchState; status starts as
197
+ // 'pending' (not 'running') so getStatusUpdate() runningTasks narration is skipped.
198
+ query: '',
199
+ status: 'pending',
200
+ startedAt: Date.now(),
201
+ progressUpdates: [],
202
+ });
203
+ }
204
+ const task = this.tasks.get(id);
205
+ if (fields.owner !== undefined)
206
+ task.owner = fields.owner;
207
+ if (fields.subagentType !== undefined)
208
+ task.subagentType = fields.subagentType;
209
+ if (fields.dispatchState !== undefined)
210
+ task.dispatchState = fields.dispatchState;
211
+ if (fields.artifact !== undefined)
212
+ task.artifact = fields.artifact;
213
+ // Mirror dispatchState into the base status field so existing getStatusUpdate() callers see it.
214
+ // 'rejected' is also terminal — map it to 'failed' so the GC and readers treat it as finished.
215
+ if (fields.dispatchState === 'completed')
216
+ task.status = 'completed';
217
+ if (fields.dispatchState === 'failed' || fields.dispatchState === 'rejected')
218
+ task.status = 'failed';
219
+ // Fix: set completedAt when entering any terminal state so the task is GC-eligible via
220
+ // clearReportedTasks() and correctly surfaces in hasCompletedTasks() / getStatusUpdate().
221
+ // Without this the tasks Map grows unbounded (memory leak).
222
+ const isTerminal = task.status === 'completed' || task.status === 'failed';
223
+ if (isTerminal && !task.completedAt)
224
+ task.completedAt = Date.now();
225
+ console.log(`[Dispatch] upsertDispatch id=${id.slice(0, 8)} state=${fields.dispatchState ?? '-'} type=${fields.subagentType ?? '-'}`);
226
+ }
185
227
  }
186
228
  // Singleton instance
187
229
  export const statusManager = new StatusManager();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.141",
3
+ "version": "0.9.143",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {