osborn 0.9.238 → 0.9.239

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.
@@ -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
  }],
@@ -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.239",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {