agent-relay-runner 0.129.11 → 0.129.13

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.
@@ -2,31 +2,34 @@ import { hostname } from "node:os";
2
2
  import { mkdirSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import type { AgentLifecycle, AgentProfile, ContextState, Message, MessageSessionMeta, ProviderState, SendMessageInput, TaskStatusInput, WorkspaceMetadata } from "agent-relay-sdk";
5
- import { errMessage, RelayBusClient, RelayHttpClient } from "agent-relay-sdk";
5
+ import { errMessage, RelayBusClient, RelayHttpClient, isCodexLeanWorkerProfile, CODEX_WORKER_ONDEMAND_TOOLS } from "agent-relay-sdk";
6
6
  import { contextStateFromProbeMetrics, quotaStateFromProbeMetrics } from "agent-relay-sdk/context-probe";
7
7
  import { isPidAlive } from "agent-relay-sdk/process-utils";
8
8
  import { providerAttachmentText, providerMessageText } from "./adapter";
9
9
  import { computeLivenessSignal, type LivenessInputs } from "./liveness";
10
- import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissionDecision, ProviderPermissionDecisionInput, ProviderSessionEvent, ProviderSessionEventBatch, ProviderSessionTurnContext, ProviderSessionTurnInput, ProviderStatusUpdate, ProviderUserPromptInput, RunnerSpawnConfig, SemanticStatus, TerminalAttachSpec } from "./adapter";
10
+ import type { ManagedProcess, PaneObservation, ProviderAdapter, ProviderConfig, ProviderPermissionDecision, ProviderPermissionDecisionInput, ProviderSessionEvent, ProviderSessionEventBatch, ProviderSessionTurnContext, ProviderSessionTurnInput, ProviderStatusUpdate, ProviderUserPromptInput, RunnerSpawnConfig, SemanticStatus, TerminalAttachSpec } from "./adapter";
11
11
  import { messagesWithCachedAttachments } from "./attachment-cache";
12
12
  import { ClaimTracker } from "./claim-tracker";
13
13
  import { ProviderQuotaHarvest } from "./adapters/provider-quota-harvest";
14
14
  import { readRunnerContextProbeState } from "./context-probe-state";
15
+ import { transcriptDerivedContext, type TranscriptContextCache } from "./transcript-context";
15
16
  import { startControlServer, type ControlServer, type ResolvedPermissionPrompt } from "./control-server";
16
17
  import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
17
18
  import { Outbox, type OutboxRecord } from "./outbox";
18
19
  import { getManifest } from "agent-relay-providers";
19
20
  import { profileUsesProviderHostGlobals } from "./profile-home";
20
21
  import { RELAY_MCP_TOKEN_ENV, relayMcpEndpoint, resolveSharedMcpUrl } from "./relay-mcp";
21
- import { RelayMcpProxy } from "./relay-mcp-proxy";
22
+ import { RelayMcpProxy, isolatedWorkspaceDescriptor } from "./relay-mcp-proxy";
22
23
  import { runtimeMetadata } from "./version";
23
24
  import { logger, parseLogLevel } from "./logger";
24
25
  import { ensureSessionScratch, reapSessionLauncher, reapSessionScratch, sweepStaleLaunchers, sweepStaleSessions, type SessionScratchLayout } from "./session-scratch";
25
26
  import { deliverBufferedMcpCall as deliverBufferedMcpOutboxCall, deliverRunnerOutboxEvent } from "./mcp-outbox";
26
27
  import { RunnerInsights } from "./runner-insights";
27
28
  import { BusyReconciler } from "./busy-reconciler";
29
+ import { reconciledTurnTimelineEvent } from "./turn-reconcile-event";
28
30
  import { publishCapturedResponse } from "./response-capture-report";
29
31
  import { PendingPermissionStore, type PendingPermissionRequestRecord } from "./pending-permission-store";
32
+ import { ProviderInputQueue, type ProviderInputTicket } from "./provider-input-queue";
30
33
  import { runnerLaunchInjectionSessionEvents, type RunnerRelayInjectionEvent } from "./relay-injection-events";
31
34
  import { providerTerminalSession, providerTerminalSocket } from "./process-meta";
32
35
  import { capsFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
@@ -41,7 +44,9 @@ import {
41
44
  isContextState,
42
45
  isHttpAuthError,
43
46
  isHttpStatusError,
47
+ isTurnInProgressDeliveryError,
44
48
  lifecycleCapabilities,
49
+ PROVIDER_INPUT_COMMAND_TYPES,
45
50
  providerExitLastError,
46
51
  providerExitDiagnostics,
47
52
  providerExitResumeId,
@@ -183,6 +188,14 @@ export class AgentRunner {
183
188
  // delivery (and vice versa); its backoff is capped low since a stale live-mirror frame is cheap.
184
189
  private readonly sessionOutbox: Outbox;
185
190
  private readonly pendingPermissionStore: PendingPermissionStore;
191
+ // #1565 F1 — the one gate in front of the provider's single input channel. Every payload the
192
+ // runner delivers (message batch, injected prompt, self-resume continuation, memory preamble)
193
+ // passes through it, so two payloads for one agent can never interleave at the wire. FIFO, with
194
+ // the place in line taken synchronously at dispatch, so the payload the relay created first is
195
+ // the one delivered first.
196
+ private readonly providerInput = new ProviderInputQueue({
197
+ onStuck: (heldMs) => logger.warn("delivery", `provider input stream held for ${heldMs}ms; force-releasing so delivery cannot wedge`),
198
+ });
186
199
  private sessionBatchSeq = 0;
187
200
  private readonly insights: RunnerInsights;
188
201
  private readonly busyReconciler: BusyReconciler;
@@ -232,6 +245,14 @@ export class AgentRunner {
232
245
  // Last transcript path seen this session — used by end-of-session Insights (#184)
233
246
  // when the SessionEnd hook payload omits it.
234
247
  private lastTranscriptPath?: string;
248
+ // #1512: mtime-keyed cache of the last transcript-derived context usage, so the
249
+ // heartbeat's fallback parse doesn't re-read a large transcript that hasn't changed.
250
+ private transcriptContextCache?: TranscriptContextCache;
251
+ // #1235 — true while a monitorless (hookless) provider turn is being mirrored natively:
252
+ // the adapter supplied a transcript path on the provider-turn busy edge, so the runner
253
+ // drives the reasoning tail itself (start on busy, final-capture on idle) instead of via
254
+ // Stop/UserPromptSubmit hooks the lean/isolated worker never installs.
255
+ private nativeSessionCaptureActive = false;
235
256
  private lifecycleAction?: LifecycleAction;
236
257
  // Coalesces concurrent pre-session-destroy runs (e.g. the shutdown bus command and the
237
258
  // SessionEnd hook both fire for the same teardown) so the cursor isn't raced.
@@ -423,8 +444,45 @@ export class AgentRunner {
423
444
  }
424
445
 
425
446
  private registerProviderSessionRoutes(): void {
426
- this.options.adapter.onSessionEvent?.((event) => { void this.publishProviderSessionEvent(event); });
427
- this.options.adapter.onSessionEvents?.((events) => { void this.publishProviderSessionEvents(events); });
447
+ // (#1617) neither publisher has a try/catch of its own, and both bottom out in
448
+ // sessionOutbox.enqueue: a synchronous bun:sqlite INSERT that throws outright once the outbox
449
+ // is closed (teardown ordering in stop(): the adapter keeps emitting after close) or the
450
+ // database errors. This is the highest-frequency fire-and-forget site in the runner — every
451
+ // provider session event passes through it — so leaving it unguarded is the likeliest way a
452
+ // live agent silently stops existing (Bun exits(1) on an unhandled rejection, #1614).
453
+ this.options.adapter.onSessionEvent?.((event) => { void this.publishProviderSessionEvent(event).catch((error) => this.reportBackgroundRejection("provider session event publish", "session.publish_failed", "Provider session event not mirrored", error)); });
454
+ this.options.adapter.onSessionEvents?.((events) => { void this.publishProviderSessionEvents(events).catch((error) => this.reportBackgroundRejection("provider session event batch publish", "session.publish_failed", "Provider session events not mirrored", error)); });
455
+ }
456
+
457
+ // The adapter's status seam, split out of run() (#1617) so the guarded restart call below is
458
+ // reachable without booting a whole runner — the crash it guards against is exactly the one a
459
+ // test has to be able to drive.
460
+ /**
461
+ * #1651 — the terminus of the pane-diagnostic channel. It writes to the session log and
462
+ * stops. This method exists to be boring: it is the proof that a pane reading has somewhere
463
+ * to go OTHER than setProviderStatus, which is what let the five prompt gates and the
464
+ * connection-retry detector be stripped of their authority without going silent. If a future
465
+ * change makes this mutate anything, it has re-created the defect class #1651 closed.
466
+ */
467
+ private recordPaneObservation(observation: PaneObservation): void {
468
+ const detail = observation.message ? `: ${observation.message}` : "";
469
+ this.sessionLog(`pane observation (advisory, no state change) ${observation.eventType}/${observation.detectorId}${detail}`);
470
+ }
471
+
472
+ private handleAdapterStatusChange(status: ProviderStatusUpdate): void {
473
+ if (this.restartInProgress || this.restartPending) return;
474
+ const semanticStatus = typeof status === "string" ? status : status.status;
475
+ if (this.shouldRestartUnexpectedProviderExit(semanticStatus)) {
476
+ // (#1617) — restartUnexpectedProviderExit is an outer try/finally with NO catch wrapped
477
+ // around awaited work that can still throw (recoverNativeSelfResumeExit, the undetermined-
478
+ // exit confirmation read, the timeline/status publishes). A reject here used to kill the
479
+ // runner outright: the crash path for the exact situation this method exists to recover
480
+ // from, on a runner nothing respawns (#1618).
481
+ void this.restartUnexpectedProviderExit(semanticStatus).catch((error) => this.reportBackgroundRejection("unexpected provider exit restart", "provider.restart_failed", "Provider restart after unexpected exit failed", error));
482
+ return;
483
+ }
484
+ this.setProviderStatus(status);
485
+ if (runnerShouldResolveProviderExit(semanticStatus, this.exitCommandInProgress)) this.options.onProviderExit?.(semanticStatus === "offline" ? 0 : 1);
428
486
  }
429
487
 
430
488
  async run(): Promise<void> {
@@ -449,16 +507,8 @@ export class AgentRunner {
449
507
  });
450
508
  this.startMcpProxy();
451
509
  this.writeRunnerInfoFile();
452
- this.options.adapter.onStatusChange((status) => {
453
- if (this.restartInProgress || this.restartPending) return;
454
- const semanticStatus = typeof status === "string" ? status : status.status;
455
- if (this.shouldRestartUnexpectedProviderExit(semanticStatus)) {
456
- void this.restartUnexpectedProviderExit(semanticStatus);
457
- return;
458
- }
459
- this.setProviderStatus(status);
460
- if (runnerShouldResolveProviderExit(semanticStatus, this.exitCommandInProgress)) this.options.onProviderExit?.(semanticStatus === "offline" ? 0 : 1);
461
- });
510
+ this.options.adapter.onStatusChange((status) => this.handleAdapterStatusChange(status));
511
+ this.options.adapter.onPaneObservation?.((observation) => this.recordPaneObservation(observation));
462
512
  this.registerProviderSessionRoutes();
463
513
  this.bus.on("message.new", (message) => {
464
514
  // A delivered message may create a new reply obligation — warm the snapshot so the
@@ -468,7 +518,7 @@ export class AgentRunner {
468
518
  this.enqueueMessage(msg);
469
519
  });
470
520
  this.bus.on("command", (type, params, commandId, command) => {
471
- void this.handleCommand(type, params, commandId, command);
521
+ this.dispatchCommand(type, params, commandId, command);
472
522
  });
473
523
  this.bus.on("error", (code, message) => this.handleBusError(String(code), String(message)));
474
524
  await registerWithinDeadline(this.bus, registrationTimeoutMsFromEnv(), this.options.relayUrl, (reason) => logger.fatal("register", reason));
@@ -516,6 +566,7 @@ export class AgentRunner {
516
566
  this.disarmBusyLivenessReconcile();
517
567
  this.busyReconciler.disarm();
518
568
  this.stopReasoningTail();
569
+ this.nativeSessionCaptureActive = false;
519
570
  this.obligationCache.stop();
520
571
  this.outbox.close(); this.sessionOutbox.close(); this.pendingPermissionStore.close();
521
572
  this.proxy?.stop();
@@ -527,9 +578,24 @@ export class AgentRunner {
527
578
  // relay with the runner's LIVE token, buffers bufferable writes durably during a relay outage,
528
579
  // and narrows the tool list to this agent's workspace context. Best-effort: if it can't bind,
529
580
  // we fall back to a direct relay MCP connection (the agent env still works, no resilience).
581
+ // #1450 — the on-demand relay set to hide from a lean Codex worker's DIRECT relay client (so only
582
+ // the eager hot-path set boots), or undefined for any agent that keeps its full relay surface. The
583
+ // gate MIRRORS the callmux relay-downstream gate (launch-assembly) EXACTLY — worker profile, no
584
+ // client-native tool deferral, sharedMcp on (the metaOnly callmux that fronts these exists), relay
585
+ // mcp on — so the direct client is narrowed only when callmux is simultaneously fronting the set.
586
+ // ("proxy active" is implicit: only a constructed proxy consults this, and the relay downstream is
587
+ // added only when the proxy is active — the two are on/off together, never a half state.)
588
+ private codexWorkerOnDemandTools(): Set<string> | undefined {
589
+ const profile = this.options.agentProfile;
590
+ if (!isCodexLeanWorkerProfile(profile?.name) || !profile?.sharedMcp || profile?.relay?.mcp === false) return undefined;
591
+ if (getManifest(this.options.provider)?.mcpToolManifest?.clientNativeToolDeferral !== false) return undefined;
592
+ return new Set(CODEX_WORKER_ONDEMAND_TOOLS);
593
+ }
594
+
530
595
  private startMcpProxy(): void {
531
596
  if (!this.mcpProxyEnabled) return;
532
597
  try {
598
+ const onDemandTools = this.codexWorkerOnDemandTools();
533
599
  this.proxy = new RelayMcpProxy({
534
600
  relayMcpEndpoint: relayMcpEndpoint(this.options.relayUrl),
535
601
  getToken: () => this.currentToken,
@@ -541,7 +607,8 @@ export class AgentRunner {
541
607
  idempotencyKey: call.idempotencyKey,
542
608
  });
543
609
  },
544
- initialContext: { isolatedWorktree: this.ownsIsolatedWorktree() },
610
+ initialContext: { isolatedWorktree: this.ownsIsolatedWorktree(), workspaceDescriptor: isolatedWorkspaceDescriptor(this.options.workspace, this.ownsIsolatedWorktree()) },
611
+ ...(onDemandTools ? { onDemandTools } : {}),
545
612
  });
546
613
  this.mcpProxyEndpoint = this.proxy.start().url;
547
614
  logger.info("mcp-proxy", `runner MCP proxy listening at ${this.mcpProxyEndpoint} (worktree=${this.ownsIsolatedWorktree()})`);
@@ -617,6 +684,10 @@ export class AgentRunner {
617
684
  // Stage 2 (#215): the MCP endpoint the agent's client should target — the runner-local
618
685
  // proxy when active, undefined when disabled (adapters fall back to the direct relay URL).
619
686
  ...(this.mcpProxyEndpoint ? { relayMcpEndpoint: this.mcpProxyEndpoint } : {}),
687
+ // #1450: the proxy secret, exposed to the assembler ONLY when the proxy is active, so the
688
+ // Codex per-agent metaOnly callmux can reach the relay endpoint on-demand THROUGH the proxy
689
+ // (live-token-injected — never the relay token on disk, #215).
690
+ ...(this.mcpProxyEndpoint ? { mcpProxySecret: this.mcpProxySecret } : {}),
620
691
  // #672: the shared host-listener URL the `callmux bridge` connects to (env → default).
621
692
  // Always resolved; the assembler only emits the bridge when `agentProfile.sharedMcp` is on.
622
693
  sharedMcpUrl: resolveSharedMcpUrl(),
@@ -689,7 +760,7 @@ export class AgentRunner {
689
760
  this.deliveringInitialPrompt = true;
690
761
  const attempt = (this.initialPromptAttempts += 1);
691
762
  try {
692
- this.recordInjectedPrompt(prompt); await this.options.adapter.deliverInitialPrompt(this.process, prompt, { readyTimeoutMs, isInitial: true });
763
+ this.recordInjectedPrompt(prompt); await this.providerInput.run(() => this.options.adapter.deliverInitialPrompt!(this.process!, prompt, { readyTimeoutMs, isInitial: true }));
693
764
  this.pendingInitialPrompt = undefined;
694
765
  } catch (error) {
695
766
  const giveUp = attempt >= MAX_INITIAL_PROMPT_ATTEMPTS;
@@ -754,7 +825,9 @@ export class AgentRunner {
754
825
  agentId: this.agentId,
755
826
  onError: (message) => logger.error("runner", message),
756
827
  });
757
- await this.options.adapter.deliver(this.process, prepared);
828
+ // #1565 F1 — behind the same gate as injected payloads: a message batch and a boundary
829
+ // inject arriving together must not paste into each other.
830
+ await this.providerInput.run(() => this.options.adapter.deliver(this.process!, prepared));
758
831
  for (const message of deliverable) {
759
832
  await this.http.recordMessageDeliveryAttempt(message.id, { agentId: this.agentId, status: "delivered" }).catch(() => {});
760
833
  await this.http.markRead(message.id, this.agentId).catch(() => {});
@@ -774,7 +847,24 @@ export class AgentRunner {
774
847
  }
775
848
  }
776
849
 
777
- private async handleCommand(type: string, params: Record<string, unknown>, commandId: string, command?: Record<string, unknown>): Promise<void> {
850
+ /**
851
+ * The bus dispatch seam (#1565 F1).
852
+ *
853
+ * Commands that put a payload into the provider's input channel take their place in the
854
+ * `providerInput` queue HERE, synchronously, before the first `await` — so the order the relay
855
+ * created them in (continuation first, memory beside it) is the order they are delivered in, no
856
+ * matter how long each command's own bookkeeping takes. Everything else keeps dispatching
857
+ * fire-and-forget: the gate is on the wire, not on command handling.
858
+ *
859
+ * The ticket is released when the command settles even if delivery never happened (a throw before
860
+ * the deliver, an unsupported provider), so a failed command can never wedge the stream.
861
+ */
862
+ private dispatchCommand(type: string, params: Record<string, unknown>, commandId: string, command?: Record<string, unknown>): void {
863
+ const ticket = PROVIDER_INPUT_COMMAND_TYPES.has(type) ? this.providerInput.reserve() : undefined;
864
+ void this.handleCommand(type, params, commandId, command, ticket).finally(() => ticket?.release());
865
+ }
866
+
867
+ private async handleCommand(type: string, params: Record<string, unknown>, commandId: string, command?: Record<string, unknown>, ticket?: ProviderInputTicket): Promise<void> {
778
868
  const target = typeof command?.target === "string" ? command.target : this.agentId;
779
869
  if (target !== this.agentId && target !== this.options.runnerId) return;
780
870
  if (type !== "agent.shutdown" && type !== "agent.restart" && type !== "agent.reconnect" && type !== "agent.kill" && type !== "agent.compact" && type !== "agent.clearContext" && type !== "agent.injectContext" && type !== "agent.permissionDecision" && type !== "agent.interrupt" && type !== "prompt.inject") return;
@@ -814,6 +904,8 @@ export class AgentRunner {
814
904
  } else if (type === "agent.clearContext") {
815
905
  if (!this.options.adapter.clearContext || !this.process) throw new Error("provider does not support clearContext");
816
906
  providerResult = await this.options.adapter.clearContext(this.process);
907
+ const coupled = await this.deliverCoupledContinuationAfterClear(params, providerResult || undefined, ticket);
908
+ if (coupled) providerResult = { ...(providerResult ?? {}), ...coupled };
817
909
  } else if (type === "agent.interrupt") {
818
910
  if (!this.options.adapter.interrupt || !this.process) throw new Error("provider does not support interrupt");
819
911
  const redirect = params.redirect && typeof params.redirect === "object" && !Array.isArray(params.redirect)
@@ -829,11 +921,11 @@ export class AgentRunner {
829
921
  if (redirect) providerResult = { interrupt: providerResult, redirect: await this.injectPrompt(redirect) };
830
922
  } else if (type === "agent.injectContext") {
831
923
  if (!this.process) throw new Error("provider process is unavailable");
832
- providerResult = await this.injectContext(params);
924
+ providerResult = await this.injectContext(params, ticket);
833
925
  } else if (type === "agent.permissionDecision") {
834
926
  providerResult = await this.respondToPermissionDecision(params);
835
927
  } else if (type === "prompt.inject") {
836
- providerResult = await this.injectPrompt(params);
928
+ providerResult = await this.injectPrompt(params, ticket);
837
929
  } else await this.shutdownProvider(type === "agent.kill", commandTimeoutMs(params));
838
930
  await this.updateCommand(commandId, "succeeded", {
839
931
  action: type,
@@ -868,7 +960,54 @@ export class AgentRunner {
868
960
  }
869
961
  }
870
962
 
871
- private async injectContext(params: Record<string, unknown>): Promise<Record<string, unknown>> {
963
+ // #1588 couple the self-resume continuation delivery into the SAME clearContext command that
964
+ // wipes the context, so the two can never race across separate commands/transports again. The
965
+ // relay embeds the fully-built (identity-augmented) continuation in the clear command's
966
+ // `selfResume.continuation`; we deliver it in-process, strictly AFTER adapter.clearContext has
967
+ // returned. `continuationDelivered` tells the relay whether it can skip the redundant reactive
968
+ // inject or must still fire its #1023 backstop — and it MUST be truthful, because the relay skips
969
+ // BOTH backstops on continuationDelivered===true, so a false-positive true is a silent-wipe risk
970
+ // with no recovery. So `true` requires TWO things: (1) the adapter's clear-aware barrier
971
+ // POSITIVELY confirmed the TUI clear committed (clearResult.clearConfirmed === true — the strong
972
+ // clear-committed evidence), and (2) the in-process deliver resolved. A settle timeout / uncertain
973
+ // clear → clearConfirmed !== true → we still deliver in-process (best effort) but report false so
974
+ // the backstop re-injects. Uncertainty ALWAYS resolves to false (backstop covers), never true.
975
+ private async deliverCoupledContinuationAfterClear(params: Record<string, unknown>, clearResult: Record<string, unknown> | undefined, ticket?: ProviderInputTicket): Promise<Record<string, unknown> | undefined> {
976
+ // Opt-in per provider: only adapters whose clearContext is a fire-and-forget keystroke with no
977
+ // completion barrier (Claude) couple the continuation in-process. Codex's clearContext is an
978
+ // awaited atomic threadStart (the await IS the barrier), so it keeps the separate confirmed-inject
979
+ // flow untouched — never sets continuationDelivered, so the relay never skips its reactive inject.
980
+ if (!this.options.adapter.couplesClearInjectContinuation) return undefined;
981
+ const selfResume = params.selfResume !== null && typeof params.selfResume === "object" && !Array.isArray(params.selfResume)
982
+ ? params.selfResume as Record<string, unknown>
983
+ : undefined;
984
+ if (!selfResume || selfResume.mode !== "clear-inject") return undefined;
985
+ const continuation = typeof selfResume.continuation === "string" ? selfResume.continuation.trim() : "";
986
+ if (!continuation) return undefined;
987
+ // Only a genuine clear-commit confirmation from the adapter's clear-aware barrier permits a
988
+ // truthful continuationDelivered:true. Absent/false (older runner, or a settle timeout) → false.
989
+ const clearConfirmed = clearResult?.clearConfirmed === true;
990
+ try {
991
+ // #1607 N3 — reuse the outer clearContext ticket instead of letting injectContext reserve a
992
+ // fresh one, which would queue behind (and thus deadlock on) the still-held outer ticket.
993
+ await this.injectContext({ content: continuation, reason: "self-resume-continuation", selfResume }, ticket);
994
+ if (!clearConfirmed) {
995
+ // Deliver ran, but the clear was NOT positively confirmed committed — the continuation may
996
+ // have raced a still-processing clear. Report false so the relay backstop re-injects out of
997
+ // band. A redundant re-inject is always preferable to a silent wipe.
998
+ this.sessionLog("coupled self-resume continuation delivered but clear commit was NOT confirmed; relay backstop will re-inject");
999
+ }
1000
+ return { continuationDelivered: clearConfirmed };
1001
+ } catch (error) {
1002
+ // The clear already ran — reporting the command as failed would strand the agent with the
1003
+ // relay unaware its context is gone. Instead report the clear succeeded WITHOUT
1004
+ // continuationDelivered so the relay's reactive/sweep backstop re-injects out-of-band.
1005
+ this.sessionLog(`coupled self-resume continuation delivery failed, relay backstop will retry: ${errMessage(error)}`);
1006
+ return { continuationDelivered: false };
1007
+ }
1008
+ }
1009
+
1010
+ private async injectContext(params: Record<string, unknown>, ticket?: ProviderInputTicket): Promise<Record<string, unknown>> {
872
1011
  const content = typeof params.content === "string" ? params.content.trim() : "";
873
1012
  if (!content) throw new Error("content required");
874
1013
  const memoryIds = Array.isArray(params.memoryIds) ? params.memoryIds.filter((id): id is string => typeof id === "string") : [];
@@ -877,7 +1016,7 @@ export class AgentRunner {
877
1016
  ? params.selfResume as Record<string, unknown>
878
1017
  : undefined;
879
1018
  const isSelfResumeContinuation = reason === "self-resume-continuation" || Boolean(selfResume);
880
- await this.options.adapter.deliver(this.process!, [{
1019
+ const payload: Message = {
881
1020
  id: 0,
882
1021
  from: "system",
883
1022
  to: this.agentId,
@@ -893,7 +1032,24 @@ export class AgentRunner {
893
1032
  },
894
1033
  readBy: [],
895
1034
  createdAt: Date.now(),
896
- }]);
1035
+ };
1036
+ // #1565 F1 — one payload at a time, in dispatch order (see dispatchCommand). The ticket, when
1037
+ // present, is the place in line this command took the moment it arrived.
1038
+ const deliver = (slot?: ProviderInputTicket) => this.providerInput.run(() => this.options.adapter.deliver(this.process!, [payload]), slot);
1039
+ try {
1040
+ await deliver(ticket);
1041
+ } catch (error) {
1042
+ // Codex has no input buffer: a payload offered during a live turn is rejected rather than
1043
+ // queued (#1339). Serializing the boundary pair means the SECOND payload now meets the turn
1044
+ // the first one started, so re-offer it once the turn ends instead of failing the command —
1045
+ // the same queue-behind-the-turn semantics Claude's transports get for free from the CLI's
1046
+ // own buffer. A timeout waiting for the turn re-throws: the command fails, the memory
1047
+ // delta-gate does not advance, and the relay backstop re-sends. Never a silent drop.
1048
+ if (!isTurnInProgressDeliveryError(error)) throw error;
1049
+ this.sessionLog(`context injection (${reason ?? "no reason"}) deferred behind the active provider turn; re-delivering when it ends`);
1050
+ await this.busyReconciler.waitForProviderTurnClear();
1051
+ await deliver();
1052
+ }
897
1053
  return { memoryIds, injectedMemoryCount: memoryIds.length };
898
1054
  }
899
1055
 
@@ -928,7 +1084,7 @@ export class AgentRunner {
928
1084
  return { approvalId, decision, ...(result ? { providerResult: result } : {}) };
929
1085
  }
930
1086
 
931
- private async injectPrompt(params: Record<string, unknown>): Promise<Record<string, unknown>> {
1087
+ private async injectPrompt(params: Record<string, unknown>, ticket?: ProviderInputTicket): Promise<Record<string, unknown>> {
932
1088
  const body = typeof params.body === "string" ? params.body : "";
933
1089
  if (!body) throw new Error("body required");
934
1090
  if (!this.process) throw new Error("provider process is unavailable");
@@ -944,7 +1100,7 @@ export class AgentRunner {
944
1100
  this.recordInjectedPrompt(prompt.trim());
945
1101
  // #1228: prompt injection is a manual dashboard "continue"/chat-box send, not the
946
1102
  // agent's genuine first turn — never label it as the initial prompt.
947
- await this.options.adapter.deliverInitialPrompt(this.process, prompt, { isInitial: false });
1103
+ await this.providerInput.run(() => this.options.adapter.deliverInitialPrompt!(this.process!, prompt, { isInitial: false }), ticket);
948
1104
  return { injected: true, messageId, attachmentCount: attachments.length };
949
1105
  }
950
1106
 
@@ -967,7 +1123,7 @@ export class AgentRunner {
967
1123
  }
968
1124
  this.recordInjectedPrompt(text.trim());
969
1125
  // #1228: this resolves a held AskUserQuestion mid-session — never the initial prompt.
970
- await this.options.adapter.deliverInitialPrompt(this.process, text, { isInitial: false }).catch((error) => {
1126
+ await this.providerInput.run(() => this.options.adapter.deliverInitialPrompt!(this.process!, text, { isInitial: false })).catch((error) => {
971
1127
  this.sessionLog(`failed to inject AskUserQuestion answer for ${input.approvalId}: ${errMessage(error)}`);
972
1128
  });
973
1129
  }
@@ -1280,6 +1436,30 @@ export class AgentRunner {
1280
1436
  this.publishStatus();
1281
1437
  }
1282
1438
 
1439
+ // (#1617) — the reporting half of a guarded fire-and-forget call. Bun exits(1) on an unhandled
1440
+ // rejection and the runner installs no `unhandledRejection` handler, so any `void this.foo()`
1441
+ // whose target can reject takes the whole agent down — and nothing respawns it (#1618).
1442
+ //
1443
+ // Deliberately NOT a fire-and-forget wrapper: the `.catch` still has to be written out at every
1444
+ // call site, so adding one stays a visible decision rather than something a helper hides. All
1445
+ // this fixes is the SHAPE of the report — the same logger.error + publishRunnerTimelineEvent
1446
+ // pair attemptInitialPromptDelivery and the #1614 obligation re-injection already use, so a
1447
+ // background operation that dies is loud on the agent's own timeline instead of vanishing.
1448
+ private reportBackgroundRejection(scope: string, status: string, title: string, error: unknown): void {
1449
+ const body = errMessage(error);
1450
+ logger.error("runner", `${scope} failed: ${body}`);
1451
+ this.publishRunnerTimelineEvent({ status, timestamp: Date.now(), title, body });
1452
+ }
1453
+
1454
+ // (#1617) — renewRuntimeToken is an outer try/finally with no catch: the orchestrator re-mint
1455
+ // gate, publishStatus and scheduleRuntimeTokenRenewal all run unguarded after the inner
1456
+ // self-renew try. Both callers are timers/edges with nobody to await the promise, so a reject
1457
+ // reaches the top. Reported, never swallowed — a token that silently stops renewing strands the
1458
+ // agent off the bus, which is the exact failure this renewal loop exists to prevent.
1459
+ private renewRuntimeTokenInBackground(): void {
1460
+ void this.renewRuntimeToken().catch((error) => this.reportBackgroundRejection("runtime token renewal", "runtime-token-renewal-failed", "Runtime token renewal aborted", error));
1461
+ }
1462
+
1283
1463
  private async updateCommand(commandId: string, status: string, result?: Record<string, unknown>, error?: string): Promise<void> {
1284
1464
  await this.bus.updateCommand(commandId, { status, ...(result ? { result } : {}), ...(error ? { error } : {}) });
1285
1465
  }
@@ -1408,20 +1588,49 @@ export class AgentRunner {
1408
1588
  // answer/dismiss path already cleared this; this is the fallback for every other way
1409
1589
  // forward progress can happen, so a stale question never lingers past its own turn.
1410
1590
  this.pendingQuestionHold = undefined;
1591
+ // #1235 — a hookless (monitorless) adapter turn carries its live transcript path on the
1592
+ // busy edge. Drive the reasoning tail natively so lean/isolated Claude workers, which
1593
+ // never install the Stop/UserPromptSubmit hooks, still stream chat. Hook-driven agents
1594
+ // never set transcriptPath here, so their hook-triggered capture is untouched.
1595
+ const nativeTranscriptPath = typeof update !== "string" ? update.transcriptPath : undefined;
1596
+ if (nativeTranscriptPath) {
1597
+ this.lastTranscriptPath = nativeTranscriptPath;
1598
+ this.nativeSessionCaptureActive = true;
1599
+ void this.startNativeSessionCapture(nativeTranscriptPath);
1600
+ }
1411
1601
  }
1412
1602
  id = this.currentTurnId;
1413
1603
  this.busyReconciler.arm();
1414
1604
  } else if (status === "idle" && reason === "provider-turn") {
1415
1605
  id = typeof update !== "string" && update.id ? update.id : this.currentTurnId ?? reason;
1416
1606
  if (this.currentTurnId) { this.completedProviderTurns += 1; this.sessionLog(`turn ended via provider idle (turn ${this.currentTurnId})`); }
1607
+ // #1235 — for a native (hookless) turn, capture the final assistant response from the
1608
+ // transcript BEFORE tearing down the tail. Snapshot the ctx while currentTurnId is still
1609
+ // set so the closing response is tagged with the correct turn.
1610
+ const nativeFinish = this.nativeSessionCaptureActive;
1611
+ const nativeTranscriptPath = nativeFinish ? ((typeof update !== "string" ? update.transcriptPath : undefined) ?? this.lastTranscriptPath) : undefined;
1612
+ const nativeCtx = nativeFinish ? this.sessionTurnContext() : undefined;
1417
1613
  this.currentTurnId = undefined;
1418
1614
  this.currentTurnStartedAt = undefined;
1419
1615
  this.compactionMidTurn = false;
1420
1616
  this.busyReconciler.disarm();
1421
- this.stopReasoningTail();
1617
+ if (nativeFinish) {
1618
+ this.nativeSessionCaptureActive = false;
1619
+ void this.finishNativeSessionCapture(nativeTranscriptPath, nativeCtx!);
1620
+ } else {
1621
+ this.stopReasoningTail();
1622
+ }
1422
1623
  // Providers with append-system-prompt policy use lifecycle hooks for obligation handling;
1423
1624
  // others need the runner to re-inject pending obligations on idle.
1424
- if (getManifest(this.options.provider)?.launch?.managedPolicyPrompt?.alwaysOn !== "append-system-prompt") void this.reInjectPendingObligation();
1625
+ // (#1614) this delivery can suspend on the FIFO (#1607 N1) and, once resumed, reject if
1626
+ // the process died out from under it or the adapter is mid-turn; reported the same way
1627
+ // attemptInitialPromptDelivery reports its own adapter.deliver failures, so it can never
1628
+ // reach the top as an unhandled rejection (Bun exits(1) on those) and never vanish silently.
1629
+ // (#1617) folded onto the shared reportBackgroundRejection shape — same channel, same
1630
+ // status/title/body, one less hand-rolled copy of the reporting block.
1631
+ if (getManifest(this.options.provider)?.launch?.managedPolicyPrompt?.alwaysOn !== "append-system-prompt") {
1632
+ void this.reInjectPendingObligation().catch((error) => this.reportBackgroundRejection("re-inject pending obligation", "obligation.reinject_failed", "Pending reply obligation not re-injected", error));
1633
+ }
1425
1634
  }
1426
1635
  // A genuine restart back to forward progress clears the terminal-exit marker so a later offline
1427
1636
  // doesn't re-announce a stale provider exit.
@@ -1434,11 +1643,16 @@ export class AgentRunner {
1434
1643
  // kill), the reconcile clears the stuck turn → idle. A dead pid means the turn is
1435
1644
  // definitively over, so this can never falsely clear a live turn (#653's hazard).
1436
1645
  const reportedPids = typeof update === "string" ? undefined : update.pids;
1437
- const livenessPids = reportedPids?.length
1438
- ? reportedPids
1439
- : reason === "provider-turn" && typeof this.process?.pid === "number" && this.process.pid > 0
1440
- ? [this.process.pid]
1441
- : undefined;
1646
+ // #1138: process.pid is undefined for tmux-launched providers (Claude) — only
1647
+ // the adapter can resolve a real pid from the tmux session/socket. Without this,
1648
+ // an ordinary hook-driven turn tracks no pid at all, so the stale-turn backstop
1649
+ // below has nothing but elapsed time to go on.
1650
+ const fallbackPid = reason === "provider-turn"
1651
+ ? (typeof this.process?.pid === "number" && this.process.pid > 0
1652
+ ? this.process.pid
1653
+ : this.process ? this.options.adapter.resolveBusyPid?.(this.process) : undefined)
1654
+ : undefined;
1655
+ const livenessPids = reportedPids?.length ? reportedPids : (fallbackPid ? [fallbackPid] : undefined);
1442
1656
  this.claims.startWork(reason, id, typeof update === "string" ? (livenessPids ? { pids: livenessPids } : {}) : {
1443
1657
  label: update.label,
1444
1658
  role: update.role,
@@ -1479,7 +1693,15 @@ export class AgentRunner {
1479
1693
  this.rateLimitHold = hold;
1480
1694
  if (fresh) this.publishRateLimitNotice(hold);
1481
1695
  }
1482
- if (status === "idle" && this.nativeSelfResume.active) {
1696
+ // #1146: native self-resume is invoked by the agent itself mid-turn (a tool call from
1697
+ // within its own active response), so the very next idle is almost always the CALLING
1698
+ // turn's own natural end — long before Claude Code even starts processing the queued
1699
+ // `/compact <continuation>`. Resolving on any idle here raced that unrelated end against
1700
+ // the real compaction and (de facto always) won, marking the command "succeeded" and
1701
+ // firing the continuation injection before compaction ever ran — a silent no-op. Gate on
1702
+ // `compactedAt` (stamped only by the genuine PostCompact "compacted" timeline event, via
1703
+ // noteCompacted) so this only resolves once compaction has actually happened.
1704
+ if (status === "idle" && this.nativeSelfResume.active?.compactedAt !== undefined) {
1483
1705
  this.nativeSelfResume.resolve(this.nativeSelfResume.active, { resumed: true, via: "provider-idle" });
1484
1706
  }
1485
1707
  this.publishStatus();
@@ -1519,7 +1741,8 @@ export class AgentRunner {
1519
1741
  // by the RELAY_INJECTION_MARKERS prefix check, but record it explicitly as
1520
1742
  // defense-in-depth for adapters that might echo only the unformatted body text.
1521
1743
  this.recordInjectedPrompt(providerMessageText([replayMessage]));
1522
- await this.options.adapter.deliver(this.process, [replayMessage]);
1744
+ // #1607 N1 — route through the FIFO like every other deliver/deliverInitialPrompt call site.
1745
+ await this.providerInput.run(() => this.options.adapter.deliver(this.process!, [replayMessage]));
1523
1746
  // Kick off a cache refresh so a cleared obligation (agent replied) is detected
1524
1747
  // before the next idle fires — without this, the stale snapshot re-injects up to
1525
1748
  // MAX_REINJECTIONS times even after the agent has already /reply-ed.
@@ -1711,14 +1934,17 @@ export class AgentRunner {
1711
1934
  this.sessionLog(`pre-destroy capture failed: ${errMessage(error)}`);
1712
1935
  }
1713
1936
  // For exit-bound transitions the runner won't be alive afterward to drain the durable
1714
- // outbox, so block (bounded) on delivering what capture just enqueued. This runs before
1937
+ // outboxes, so block (bounded) on delivering what capture just enqueued. The session
1938
+ // outbox is separate from the Insights outbox: provider turn-final/report-up captures
1939
+ // live there, and closing it without a flush loses a transient worker's last report
1940
+ // after the reaper has already closed its work window (#1139). This runs before
1715
1941
  // handleCommand's finally deletes the agent, so the runtime token is still valid here.
1716
1942
  if (reasonExitsRunner(reason)) {
1717
1943
  try {
1718
- const delivered = await this.outbox.flush(OUTBOX_FLUSH_TIMEOUT_MS);
1719
- if (!delivered) this.sessionLog(`insights: outbox flush incomplete before ${reason} (${this.outbox.pendingCount()} pending)`);
1944
+ const [delivered, sessionDelivered] = await Promise.all([this.outbox.flush(OUTBOX_FLUSH_TIMEOUT_MS), this.sessionOutbox.flush(OUTBOX_FLUSH_TIMEOUT_MS)]);
1945
+ if (!delivered || !sessionDelivered) this.sessionLog(`teardown outbox flush incomplete before ${reason}`);
1720
1946
  } catch (error) {
1721
- this.sessionLog(`insights: outbox flush failed: ${errMessage(error)}`);
1947
+ this.sessionLog(`teardown outbox flush failed: ${errMessage(error)}`);
1722
1948
  }
1723
1949
  }
1724
1950
  })();
@@ -1832,6 +2058,7 @@ export class AgentRunner {
1832
2058
  origin: event.origin ?? "provider",
1833
2059
  ...(turnId ? { turnId } : {}),
1834
2060
  ...(event.label ? { label: event.label } : {}),
2061
+ ...(event.category ? { toolCategory: event.category } : {}),
1835
2062
  ...(event.status ? { status: event.status } : {}),
1836
2063
  ...(event.streaming !== undefined ? { streaming: event.streaming } : {}),
1837
2064
  ...(event.stepId ? { stepId: event.stepId } : {}),
@@ -1868,13 +2095,17 @@ export class AgentRunner {
1868
2095
  // deliberately leaves the reasoning tail alone so a late clear can't truncate
1869
2096
  // a turn's activity stream.
1870
2097
  private forceClearProviderTurn(reason: string): void {
1871
- if (!this.claims.activeWork().some((w) => w.kind === "provider-turn")) return;
2098
+ const work = this.claims.activeWork().find((w) => w.kind === "provider-turn");
2099
+ if (!work) return;
1872
2100
  this.sessionLog(`force-clearing stuck provider-turn (${reason})`);
2101
+ const event = reconciledTurnTimelineEvent({ turnId: this.currentTurnId ?? work.id, reason, startedAt: work.startedAt });
1873
2102
  this.claims.clearWorkKind("provider-turn");
1874
2103
  this.busyReconciler.providerTurnCleared();
1875
2104
  this.currentTurnId = undefined;
1876
2105
  this.compactionMidTurn = false;
1877
- this.publishStatus();
2106
+ // #1621: the clear is EMITTED, not silently applied — publishRunnerTimelineEvent publishes
2107
+ // the status too, so the idle edge and its explanation travel in the same frame.
2108
+ this.publishRunnerTimelineEvent(event);
1878
2109
  }
1879
2110
 
1880
2111
  // #650: reconcile pid-bearing busy work against actual process liveness. A tracked
@@ -1920,6 +2151,33 @@ export class AgentRunner {
1920
2151
  this.options.adapter.stopSessionTrace?.();
1921
2152
  }
1922
2153
 
2154
+ // #1235 — start the transcript reasoning tail for a hookless (monitorless) turn. Reuses the
2155
+ // exact adapter capture path the UserPromptSubmit hook drives for hooked agents; only the
2156
+ // trigger differs (native turn-start vs hook). A missing/old transcript makes the tail a
2157
+ // graceful no-op (poll finds nothing), so this can never crash a turn.
2158
+ private async startNativeSessionCapture(transcriptPath: string): Promise<void> {
2159
+ try {
2160
+ await this.options.adapter.handleUserPrompt?.({ prompt: "", transcriptPath }, this.sessionTurnContext());
2161
+ } catch (error) {
2162
+ this.sessionLog(`native session capture start failed: ${errMessage(error)}`);
2163
+ }
2164
+ }
2165
+
2166
+ // #1235 — capture the final assistant response for a hookless turn (the Stop hook's job for
2167
+ // hooked agents), then tear down the tail. captureSessionTurn drains the tail, reads the
2168
+ // closing turn text from the transcript, and emits the final `response` session event.
2169
+ private async finishNativeSessionCapture(transcriptPath: string | undefined, ctx: ProviderSessionTurnContext): Promise<void> {
2170
+ try {
2171
+ if (transcriptPath) {
2172
+ await this.options.adapter.captureSessionTurn?.({ transcriptPath, ...(ctx.turnId ? { promptId: ctx.turnId } : {}) }, ctx);
2173
+ }
2174
+ } catch (error) {
2175
+ this.sessionLog(`native session capture finish failed: ${errMessage(error)}`);
2176
+ } finally {
2177
+ this.stopReasoningTail();
2178
+ }
2179
+ }
2180
+
1923
2181
  // Mirror a discreet, durable "context compacted" marker into chat via the existing
1924
2182
  // session-mirror lane (not a parallel channel). `notice` renders as an inline timeline
1925
2183
  // marker (never a bubble) and survives reload, unlike the ephemeral timeline-status one.
@@ -1966,7 +2224,7 @@ export class AgentRunner {
1966
2224
 
1967
2225
  private isStaleProviderTurnOnlyBusy(
1968
2226
  status: SemanticStatus,
1969
- activeWork: Array<{ kind: string }>,
2227
+ activeWork: Array<{ kind: string; pids?: number[] }>,
1970
2228
  busyReasons: string[],
1971
2229
  liveness: { hasLiveWork: boolean; lastProgressAt: number },
1972
2230
  ): boolean {
@@ -1974,6 +2232,13 @@ export class AgentRunner {
1974
2232
  if (activeWork.length === 0 || activeWork.some((work) => work.kind !== "provider-turn")) return false;
1975
2233
  if (busyReasons.some((reason) => reason !== "provider-turn")) return false;
1976
2234
  if (liveness.hasLiveWork || !liveness.lastProgressAt) return false;
2235
+ // #1138: a turn whose tracked pid is still alive is definitively still running — a
2236
+ // silent single Claude turn (no subagents, no compaction event) must not be
2237
+ // force-healed to idle purely because it's been quiet past the threshold. Only fall
2238
+ // back to the elapsed-time heuristic when there's no pid evidence to check; the #650
2239
+ // reconcile independently clears a confirmed-dead pid's work the instant it dies, so
2240
+ // this can never mask a genuine leak.
2241
+ if (activeWork.some((work) => work.pids?.some((pid) => isPidAlive(pid)))) return false;
1977
2242
  return Date.now() - liveness.lastProgressAt >= STALE_PROVIDER_TURN_BUSY_MS;
1978
2243
  }
1979
2244
 
@@ -2108,7 +2373,7 @@ export class AgentRunner {
2108
2373
  if (this.reactiveTokenRecoveryAt && now - this.reactiveTokenRecoveryAt < REACTIVE_TOKEN_RECOVERY_DEBOUNCE_MS) return;
2109
2374
  this.reactiveTokenRecoveryAt = now;
2110
2375
  this.logRunnerDiagnostic(`[runner] relay auth failure on ${source}; recovering runtime token`);
2111
- void this.renewRuntimeToken();
2376
+ this.renewRuntimeTokenInBackground();
2112
2377
  }
2113
2378
 
2114
2379
  private logHttpLivenessFailure(error: unknown, authFailed: boolean): void {
@@ -2195,7 +2460,7 @@ export class AgentRunner {
2195
2460
  this.scheduleRuntimeTokenRenewal();
2196
2461
  return;
2197
2462
  }
2198
- void this.renewRuntimeToken();
2463
+ this.renewRuntimeTokenInBackground();
2199
2464
  }, schedule.delayMs);
2200
2465
  }
2201
2466
 
@@ -2325,7 +2590,18 @@ export class AgentRunner {
2325
2590
  const processContext = this.latestProcessContext();
2326
2591
  const probeMetrics = this.latestProbeMetrics();
2327
2592
  const probeContext = probeMetrics ? contextStateFromProbeMetrics(probeMetrics) : undefined;
2328
- const context = processContext ?? probeContext;
2593
+ let context = processContext ?? probeContext;
2594
+ // #1512: when a provider has no event-stream context of its own, fall back to
2595
+ // the transcript so context.lastUpdatedAt keeps advancing after an in-process
2596
+ // compact/clear/native-resume freezes the status-line probe. Freshest wins, so
2597
+ // a live probe still takes precedence (source stays "statusline").
2598
+ if (!processContext) {
2599
+ const derived = transcriptDerivedContext({ transcriptPath: this.lastTranscriptPath, derive: this.options.adapter.contextUsageFromTranscript, tokensMax: probeMetrics?.tokensMax, cache: this.transcriptContextCache, now: Date.now() });
2600
+ this.transcriptContextCache = derived.cache;
2601
+ if (derived.context && (!probeContext || derived.context.lastUpdatedAt > probeContext.lastUpdatedAt)) {
2602
+ context = derived.context;
2603
+ }
2604
+ }
2329
2605
  const quota = probeMetrics ? quotaStateFromProbeMetrics(probeMetrics) : undefined;
2330
2606
  const terminalSession = providerTerminalSession(this.process);
2331
2607
  const terminalSocket = providerTerminalSocket(this.process);