osborn 0.9.237 → 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.
@@ -120,10 +120,17 @@ function extractSpeedMarker(text) {
120
120
  }
121
121
  function stripMarkdownForTTS(text) {
122
122
  return text
123
- // Remove code blocks (``` ... ```)
123
+ // Remove PAIRED code blocks (``` ... ```)
124
124
  .replace(/```[\s\S]*?```/g, ' [code block] ')
125
+ // Remove an UNCLOSED/partial fence to end-of-string. Assistant text streams as multiple
126
+ // blocks per turn and each block is stripped in isolation, so a fence that opens in one
127
+ // block (or is never closed) would otherwise leak raw code + literal backticks to TTS.
128
+ .replace(/```[\s\S]*$/g, ' [code block] ')
125
129
  // Remove inline code (` ... `)
126
130
  .replace(/`([^`]+)`/g, '$1')
131
+ // Sweep any remaining stray backtick runs (unbalanced ticks from split chunks) so they're
132
+ // never spoken aloud.
133
+ .replace(/`+/g, ' ')
127
134
  // Remove bold (**text** or __text__)
128
135
  .replace(/\*\*([^*]+)\*\*/g, '$1')
129
136
  .replace(/__([^_]+)__/g, '$1')
@@ -912,6 +919,11 @@ export function decideWrite(agentType, filePath) {
912
919
  // when the keyed sub-agent's Task completes, so the main agent dispatches the
913
920
  // next verifier(s) as real Tasks (verdicts return inline). Includes RUNNING the
914
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.] ';
915
927
  export const VERIFIER_CHAIN = {
916
928
  writer: {
917
929
  parallelAtStart: ['tester'],
@@ -1473,6 +1485,18 @@ export class ClaudeLLM extends llm.LLM {
1473
1485
  hasSession() {
1474
1486
  return this.#persistentQuery !== null && !this.#messageChannel?.closed;
1475
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
+ }
1476
1500
  /**
1477
1501
  * Close the persistent session (kills subprocess).
1478
1502
  * Call on disconnect, session switch, or recovery.
@@ -1948,23 +1972,12 @@ class ClaudeLLMStream extends llm.LLMStream {
1948
1972
  console.log('[LIFECYCLE-PROBE] PostToolUse on', toolName, 'agent_id=', agentId, 'ts=', Date.now());
1949
1973
  }
1950
1974
  this.#eventEmitter.emit('tool_result', { name: toolName, input: toolInput, response: toolResponse, agentRole: agentTypePost || 'main' });
1951
- // Pattern #1 verifier chaining: when the MAIN agent's Task sub-agent of a
1952
- // chained type finishes, inject a reminder so the main agent dispatches the
1953
- // next verifier(s) as real Task sub-agents (verdicts return inline). Gated to
1954
- // the main thread (agent_type absent) so verifiers never re-chain themselves.
1955
- if (!agentTypePost && (toolName === 'Task' || toolName === 'Agent')) {
1956
- const subType = String(toolInput?.subagent_type || toolInput?.agent_type || '');
1957
- const link = subType ? VERIFIER_CHAIN[subType] : undefined;
1958
- if (link?.afterReminder) {
1959
- console.log(`[DISPATCH] Pattern#1 chain: ${subType} done → reminding main agent to verify`);
1960
- return {
1961
- hookSpecificOutput: {
1962
- hookEventName: 'PostToolUse',
1963
- additionalContext: link.afterReminder,
1964
- },
1965
- };
1966
- }
1967
- }
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.
1968
1981
  return {};
1969
1982
  }]
1970
1983
  }],
@@ -2320,9 +2333,32 @@ class ClaudeLLMStream extends llm.LLMStream {
2320
2333
  const aid = input?.agent_id ?? ('sa-' + Date.now());
2321
2334
  statusManager.upsertDispatch(aid, { subagentType: at, dispatchState: 'completed', artifact: msg });
2322
2335
  this.#eventEmitter.emit('task_completed', { agent_type: at, agent_id: aid, last_assistant_message: String(msg).slice(0, 400) });
2323
- // Verifier chaining is Pattern #1 now: the PostToolUse-on-Task hook injects a
2324
- // reminder into the MAIN agent, which dispatches the reviewer/tester as real
2325
- // 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
+ }
2326
2362
  return {};
2327
2363
  }]
2328
2364
  }],
package/dist/env-keys.js CHANGED
@@ -35,6 +35,7 @@ export const AGENT_ENV_KEYS = [
35
35
  { key: 'OSBORN_CWD', required: false, explicit: true, description: 'Agent working directory.' },
36
36
  { key: 'OSBORN_SYNC_TOKEN', required: false, explicit: true, description: 'Session export/import auth token.' },
37
37
  { key: 'OSBORN_FRONTEND_URL', required: false, explicit: true, description: 'Frontend base URL for artifact upload.' },
38
+ { key: 'OSBORN_HOUSEKEEPING_DRYRUN', required: false, explicit: true, description: 'Set "0" to enable live housekeeping deletion; any other value / unset = dry-run (log only).' },
38
39
  { key: 'DEV_DOMAIN', required: false, explicit: true, description: 'Wildcard dev-routing domain.' },
39
40
  ];
40
41
  /** Keys the agent treats as required — absence should be surfaced at startup. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.237",
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": {