osborn 0.9.238 → 0.9.240

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.
@@ -63,6 +63,21 @@ export declare function runClaudeAuthFlow(callbacks: ClaudeAuthCallbacks): {
63
63
  handle: ClaudeAuthHandle;
64
64
  done: Promise<void>;
65
65
  };
66
+ /**
67
+ * Re-deliver the current in-flight auth prompt to a frontend that has just
68
+ * (re)subscribed. WHY THIS EXISTS: ensureClaudeAuth runs inside the
69
+ * ParticipantConnected ("User joined") handler and publishes claude_auth_*
70
+ * over the LiveKit reliable data channel — but that fires ~5-6s BEFORE the
71
+ * browser's data channel actually finishes subscribing (observed: auth_url at
72
+ * :01, TrackSubscribed at :07). LiveKit reliable data is NOT buffered for a
73
+ * peer that hasn't subscribed yet, so a no-auth user's one-shot "Connect
74
+ * Claude" prompt is published to nobody and the modal never appears. Calling
75
+ * this on TrackSubscribed (data channel confirmed up) re-delivers the exact
76
+ * cached state. No-op (returns false) when no flow is in-flight — i.e. the
77
+ * user is already authenticated — so it costs nothing on the common path.
78
+ * Idempotent on the frontend (re-setting the same auth status is harmless).
79
+ */
80
+ export declare function replayInFlightAuth(send: (type: string, payload: unknown) => void): boolean;
66
81
  /**
67
82
  * Ensure Claude is authenticated before proceeding.
68
83
  *
@@ -485,6 +485,36 @@ export function runClaudeAuthFlow(callbacks) {
485
485
  return { handle, done };
486
486
  }
487
487
  let inFlightAuth = null;
488
+ /**
489
+ * Re-deliver the current in-flight auth prompt to a frontend that has just
490
+ * (re)subscribed. WHY THIS EXISTS: ensureClaudeAuth runs inside the
491
+ * ParticipantConnected ("User joined") handler and publishes claude_auth_*
492
+ * over the LiveKit reliable data channel — but that fires ~5-6s BEFORE the
493
+ * browser's data channel actually finishes subscribing (observed: auth_url at
494
+ * :01, TrackSubscribed at :07). LiveKit reliable data is NOT buffered for a
495
+ * peer that hasn't subscribed yet, so a no-auth user's one-shot "Connect
496
+ * Claude" prompt is published to nobody and the modal never appears. Calling
497
+ * this on TrackSubscribed (data channel confirmed up) re-delivers the exact
498
+ * cached state. No-op (returns false) when no flow is in-flight — i.e. the
499
+ * user is already authenticated — so it costs nothing on the common path.
500
+ * Idempotent on the frontend (re-setting the same auth status is harmless).
501
+ */
502
+ export function replayInFlightAuth(send) {
503
+ if (!inFlightAuth)
504
+ return false;
505
+ send('claude_auth_required', {
506
+ message: 'Claude authentication required. A login URL will appear shortly.',
507
+ });
508
+ if (inFlightAuth.lastUrl) {
509
+ send('claude_auth_url', { url: inFlightAuth.lastUrl });
510
+ }
511
+ if (inFlightAuth.lastStatus === 'waiting_code') {
512
+ send('claude_auth_waiting_code', {
513
+ message: 'Paste the authentication code from the browser.',
514
+ });
515
+ }
516
+ return true;
517
+ }
488
518
  /**
489
519
  * Ensure Claude is authenticated before proceeding.
490
520
  *
@@ -120,6 +120,7 @@ export declare function decideWrite(agentType: string | null, filePath: string):
120
120
  decision: 'allow' | 'defer' | 'deny';
121
121
  reason?: string;
122
122
  };
123
+ export declare const SILENT_VERIFY_PREFIX: string;
123
124
  export declare const VERIFIER_CHAIN: Record<string, {
124
125
  parallelAtStart?: string[];
125
126
  afterReminder?: string;
@@ -278,6 +279,13 @@ export declare class ClaudeLLM extends llm.LLM {
278
279
  stopAgent(agentId: string): boolean;
279
280
  /** Whether a persistent session is alive and consuming messages */
280
281
  hasSession(): boolean;
282
+ /**
283
+ * Verifier-chain dedup: records that agent_id has triggered its chain dispatch and returns
284
+ * true only the FIRST time (so a double-fired SubagentStop can't double-dispatch). A NEW
285
+ * agent_id (e.g. a fresh writer spawned for a fix round) is not in the set → returns true,
286
+ * correctly allowing the re-verify loop. Cleared when the persistent session closes.
287
+ */
288
+ markChainDispatched(agentId: string): boolean;
281
289
  /**
282
290
  * Close the persistent session (kills subprocess).
283
291
  * Call on disconnect, session switch, or recovery.
@@ -919,6 +919,11 @@ export function decideWrite(agentType, filePath) {
919
919
  // when the keyed sub-agent's Task completes, so the main agent dispatches the
920
920
  // next verifier(s) as real Tasks (verdicts return inline). Includes RUNNING the
921
921
  // tester's tests against the finished code. Any agent not listed chains to nothing.
922
+ // Prepended to a verifier-chain reminder pushed into the MAIN agent at SubagentStop. Keeps the
923
+ // dispatch SILENT — the model should emit ONLY the Task tool call(s), no spoken/streamed text —
924
+ // so the chain advances without a robotic "now running the review…" narration on the voice path.
925
+ export const SILENT_VERIFY_PREFIX = '[SYSTEM — SILENT CONTROL MESSAGE. Do NOT narrate, speak, or emit any assistant text this turn. ' +
926
+ 'Silently perform the following via the Task tool only.] ';
922
927
  export const VERIFIER_CHAIN = {
923
928
  writer: {
924
929
  parallelAtStart: ['tester'],
@@ -1480,6 +1485,18 @@ export class ClaudeLLM extends llm.LLM {
1480
1485
  hasSession() {
1481
1486
  return this.#persistentQuery !== null && !this.#messageChannel?.closed;
1482
1487
  }
1488
+ /**
1489
+ * Verifier-chain dedup: records that agent_id has triggered its chain dispatch and returns
1490
+ * true only the FIRST time (so a double-fired SubagentStop can't double-dispatch). A NEW
1491
+ * agent_id (e.g. a fresh writer spawned for a fix round) is not in the set → returns true,
1492
+ * correctly allowing the re-verify loop. Cleared when the persistent session closes.
1493
+ */
1494
+ markChainDispatched(agentId) {
1495
+ if (!agentId || this.#dispatchedFor.has(agentId))
1496
+ return false;
1497
+ this.#dispatchedFor.add(agentId);
1498
+ return true;
1499
+ }
1483
1500
  /**
1484
1501
  * Close the persistent session (kills subprocess).
1485
1502
  * Call on disconnect, session switch, or recovery.
@@ -1955,23 +1972,12 @@ class ClaudeLLMStream extends llm.LLMStream {
1955
1972
  console.log('[LIFECYCLE-PROBE] PostToolUse on', toolName, 'agent_id=', agentId, 'ts=', Date.now());
1956
1973
  }
1957
1974
  this.#eventEmitter.emit('tool_result', { name: toolName, input: toolInput, response: toolResponse, agentRole: agentTypePost || 'main' });
1958
- // Pattern #1 verifier chaining: when the MAIN agent's Task sub-agent of a
1959
- // chained type finishes, inject a reminder so the main agent dispatches the
1960
- // next verifier(s) as real Task sub-agents (verdicts return inline). Gated to
1961
- // the main thread (agent_type absent) so verifiers never re-chain themselves.
1962
- if (!agentTypePost && (toolName === 'Task' || toolName === 'Agent')) {
1963
- const subType = String(toolInput?.subagent_type || toolInput?.agent_type || '');
1964
- const link = subType ? VERIFIER_CHAIN[subType] : undefined;
1965
- if (link?.afterReminder) {
1966
- console.log(`[DISPATCH] Pattern#1 chain: ${subType} done → reminding main agent to verify`);
1967
- return {
1968
- hookSpecificOutput: {
1969
- hookEventName: 'PostToolUse',
1970
- additionalContext: link.afterReminder,
1971
- },
1972
- };
1973
- }
1974
- }
1975
+ // Pattern #1 verifier chaining has MOVED to the SubagentStop hook below. Reason:
1976
+ // sub-agents run with run_in_background:true, so PostToolUse-on-Task fires at
1977
+ // DISPATCH (placeholder tool_response), not completion — injecting the "now verify"
1978
+ // reminder here fired while the writer/researcher was still running (mistimed).
1979
+ // SubagentStop is the true completion signal; it pushes the reminder into the main
1980
+ // agent via pushMessage.
1975
1981
  return {};
1976
1982
  }]
1977
1983
  }],
@@ -2327,9 +2333,32 @@ class ClaudeLLMStream extends llm.LLMStream {
2327
2333
  const aid = input?.agent_id ?? ('sa-' + Date.now());
2328
2334
  statusManager.upsertDispatch(aid, { subagentType: at, dispatchState: 'completed', artifact: msg });
2329
2335
  this.#eventEmitter.emit('task_completed', { agent_type: at, agent_id: aid, last_assistant_message: String(msg).slice(0, 400) });
2330
- // Verifier chaining is Pattern #1 now: the PostToolUse-on-Task hook injects a
2331
- // reminder into the MAIN agent, which dispatches the reviewer/tester as real
2332
- // Task sub-agents (verdicts return inline). No out-of-band spawns from here.
2336
+ // Pattern #1 verifier chaining — CORRECT timing (moved here from PostToolUse).
2337
+ // SubagentStop fires at REAL completion. When a chained producer (writer/researcher)
2338
+ // finishes, push its afterReminder into the MAIN agent's streaming input so the main
2339
+ // agent dispatches the next verifier(s) as real Task sub-agents (verdicts return
2340
+ // inline). Only writer/researcher are VERIFIER_CHAIN keys with afterReminder, so
2341
+ // verifiers (reviewer/tester/reasoner) never re-chain. Dedup on agent_id guards a
2342
+ // double-fired SubagentStop; a fresh fix-round writer has a NEW id → re-chains (the
2343
+ // intended bounded loop). Guarded on a live session so we never cold-start a query
2344
+ // just to deliver a reminder. Silent-dispatch prefix keeps it off the voice.
2345
+ const chainLink = at ? VERIFIER_CHAIN[at] : undefined;
2346
+ // Dedup MUST key on the REAL agent_id, never the fabricated 'sa-<ts>' fallback used
2347
+ // above for status/emit — a synthetic id is unique every call and would defeat the
2348
+ // double-fire guard. If the SDK omits agent_id we can't dedup safely, so skip chaining.
2349
+ if (chainLink?.afterReminder && input?.agent_id && this.#llmRef.hasSession() && this.#llmRef.markChainDispatched(input.agent_id)) {
2350
+ console.log(`[DISPATCH] Pattern#1 chain (SubagentStop): ${at} done → pushing verify reminder to main agent`);
2351
+ try {
2352
+ this.#llmRef.pushMessage(SILENT_VERIFY_PREFIX + chainLink.afterReminder, sdkOptions, {
2353
+ onSessionId: this.#onSessionId,
2354
+ onCheckpoint: this.#onCheckpoint,
2355
+ eventEmitter: this.#eventEmitter,
2356
+ });
2357
+ }
2358
+ catch (e) {
2359
+ console.warn('Pattern#1 chain push failed:', e);
2360
+ }
2361
+ }
2333
2362
  return {};
2334
2363
  }]
2335
2364
  }],
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ import { cleanupHousekeeping } from './housekeeping.js';
38
38
  import { createSTT, createTTS } from './voice-io.js';
39
39
  import { createClaudeLLM, NAMED_AGENTS, applyTurbo } from './claude-llm.js';
40
40
  import { clearPipelineFastBrainSession, prewarmBM25Index } from './pipeline-fastbrain.js';
41
- import { ensureClaudeAuth, applyAuthFallback } from './claude-auth.js';
41
+ import { ensureClaudeAuth, applyAuthFallback, replayInFlightAuth } from './claude-auth.js';
42
42
  import { createSmitheryProxy, destroySmitheryProxy, parseSmitheryUrl, isSmitheryUrl, SmitheryAuthorizationError } from './smithery-proxy.js';
43
43
  import { DIRECT_MODE_PROMPT } from './prompts.js';
44
44
  import { MCP_CATALOG } from './config.js';
@@ -4035,6 +4035,20 @@ async function main() {
4035
4035
  room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
4036
4036
  try {
4037
4037
  console.log(`📥 [ROOM-TRACK-SUBSCRIBED] participant=${participant?.identity} kind=${track?.kind} source=${publication?.source} sid=${publication?.sid} t=${new Date().toISOString()}`);
4038
+ // Auth-prompt race fix: the "Connect Claude" prompt is published in the
4039
+ // ParticipantConnected handler ~5-6s before the browser's data channel
4040
+ // finishes subscribing, so a no-auth user never receives it (reliable
4041
+ // data isn't buffered for a not-yet-subscribed peer) and the modal never
4042
+ // shows. TrackSubscribed means this participant's WebRTC transport (incl.
4043
+ // data channel) is up — re-deliver any pending auth prompt now. No-op
4044
+ // when already authenticated. Covers BOTH resume and new-conversation
4045
+ // paths (both dispatch ensureClaudeAuth in the same participant handler).
4046
+ const pid = participant?.identity;
4047
+ if (typeof pid === 'string' && pid.startsWith('user-')) {
4048
+ const replayed = replayInFlightAuth((type, payload) => sendToFrontend({ type, ...payload }));
4049
+ if (replayed)
4050
+ console.log(`🔑 Re-delivered pending Claude auth prompt to ${pid} on TrackSubscribed`);
4051
+ }
4038
4052
  }
4039
4053
  catch { }
4040
4054
  });
@@ -255,16 +255,6 @@ function buildSystemPrompt(chatHistory, researchContext) {
255
255
  `Greetings/thanks/confirmations: respond directly, no search.`,
256
256
  `Tasks needing live code analysis or new research: respond with [RESEARCH_NEEDED]`,
257
257
  ``,
258
- `== DECISIONS: DEFER, DON'T ASSERT ==`,
259
- `The grounded main agent answers the SAME question in parallel with full context — it owns`,
260
- `decisions. For questions asking what was DECIDED / CHOSEN / SCOPED / AGREED / "what were the`,
261
- `options" / "what did we settle on" / "remind me what we decided": do NOT state a definitive`,
262
- `answer from one keyword hit. A single matched line is often a passing mention, not the`,
263
- `decision (e.g. a file that "covers WhatsApp and LinkedIn" is NOT the scoping decision).`,
264
- `Only assert a decision if MULTIPLE recent hits corroborate it AND one is clearly the decision`,
265
- `itself. Otherwise hedge briefly and defer — e.g. "I think it involved X, but let the main`,
266
- `agent confirm the exact decision" — or emit [RESEARCH_NEEDED]. Never contradict-with-confidence.`,
267
- ``,
268
258
  `== STYLE ==`,
269
259
  `1-3 sentences. Grounded in results. Never fabricate.`,
270
260
  `If not found after thorough searching: "I didn't find that in the session history."`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.238",
3
+ "version": "0.9.240",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {