agent-relay-runner 0.36.1 → 0.37.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.36.1",
3
+ "version": "0.37.0",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.36.1",
4
+ "version": "0.37.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/adapter.ts CHANGED
@@ -157,6 +157,13 @@ export interface ProviderAdapter {
157
157
  // `options.readyTimeoutMs` lets the runner widen the provider-ready wait for the
158
158
  // first (cold-start) delivery vs. a fast re-attempt after a ready signal (#329).
159
159
  deliverInitialPrompt?(process: ManagedProcess, prompt: string, options?: { readyTimeoutMs?: number }): Promise<void>;
160
+ // When true, the adapter seeds the spawn-time initial prompt as a launch argument
161
+ // (Claude's positional `claude "<prompt>"`), so it's already delivered the instant the
162
+ // session starts. The runner must then NOT also deliver it post-launch via
163
+ // deliverInitialPrompt — that would double-deliver and re-introduce the send-keys
164
+ // onboarding race (#352). deliverInitialPrompt stays available for mid-session injection
165
+ // (dashboard chat box) and ongoing message delivery, which have no launch-arg equivalent.
166
+ seedsInitialPromptAtLaunch?: boolean;
160
167
  deliver(process: ManagedProcess, messages: Message[]): Promise<void>;
161
168
  onStatusChange(cb: (status: ProviderStatusUpdate) => void): void;
162
169
  // Subscribe to session-mirror events from providers that emit them directly
@@ -15,6 +15,9 @@ import { claudeProviderMessageText } from "./claude-delivery";
15
15
 
16
16
  export class ClaudeAdapter implements ProviderAdapter {
17
17
  readonly provider = "claude";
18
+ // #352: initial prompt is seeded as Claude's positional launch arg (buildSpawnArgs) — reliable,
19
+ // no send-keys/onboarding race; tells the runner to skip the redundant post-launch delivery.
20
+ readonly seedsInitialPromptAtLaunch = true;
18
21
  private statusCb: (status: ProviderStatusUpdate) => void = () => {};
19
22
  private tmuxWatcher?: Timer;
20
23
  private turnWatcher?: Timer;
@@ -226,7 +229,11 @@ export class ClaudeAdapter implements ProviderAdapter {
226
229
  ...providerArgs,
227
230
  ];
228
231
  if (config.model) args.push("--model", config.model);
229
- if (config.prompt && !config.headless) args.push(String(config.prompt));
232
+ // #352: seed the prompt as Claude's positional arg for ALL launches (headless included)
233
+ // `claude "<prompt>"` seeds the first turn via Claude's own input handling, immune to the
234
+ // send-keys/onboarding race that silently dropped headless spawns. Long/special-char briefs
235
+ // are shell-safe (shellEscape + launcher-script externalization in buildTmuxArgs).
236
+ if (config.prompt) args.push(String(config.prompt));
230
237
  return {
231
238
  command,
232
239
  args,
package/src/runner.ts CHANGED
@@ -600,7 +600,13 @@ export class AgentRunner {
600
600
 
601
601
  private async deliverInitialPrompt(): Promise<void> {
602
602
  const prompt = this.options.prompt?.trim();
603
- if (prompt) await this.attemptInitialPromptDelivery(prompt, INITIAL_PROMPT_FIRST_READY_TIMEOUT_MS);
603
+ if (!prompt) return;
604
+ // #352: adapters that seed the prompt as a launch arg (Claude's positional prompt) already
605
+ // delivered it at session start. Returning BEFORE attemptInitialPromptDelivery avoids a
606
+ // double-delivery AND leaves pendingInitialPrompt unset, so the #329 ready-signal retry never
607
+ // arms — one prompt, one turn. Mid-session injectPrompt + message delivery are unaffected.
608
+ if (this.options.adapter.seedsInitialPromptAtLaunch) return;
609
+ await this.attemptInitialPromptDelivery(prompt, INITIAL_PROMPT_FIRST_READY_TIMEOUT_MS);
604
610
  }
605
611
 
606
612
  // Deliver the spawn-time first prompt, surviving a cold-start TUI that isn't input-ready yet