@agentproto/runtime 5.7.0 → 5.8.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/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ import { h as WorktreeIsolationMode, i as SpawnAttachMode, j as SpawnDedupeMode,
3
3
  export { k as DEFAULT_MODEL_ROLES, M as MODEL_ROLE_REF_PREFIX, l as ModelRoleContext, m as ModelRoleEntry, n as ModelRoleSource, o as ModelRoleValue, p as ModelRolesConfig, q as listModelRoles, r as parseModelRoleRef, s as resolveModelRole } from './config-Gz7q9yfJ.js';
4
4
  import { B as BuildHeartbeatAgent } from './heartbeat-BJtxbDb9.js';
5
5
  export { H as HeartbeatAgent, a as HeartbeatRunner, R as RuntimeEvent, b as RuntimeEvents, p as parseDuration } from './heartbeat-BJtxbDb9.js';
6
- import { M as MessageUrgency, S as SessionEvent, a as SessionEventType, b as SessionEventBus, c as SessionsRegistry, C as CompletionPolicySupervisor, T as TaskLedger, d as SessionDescriptor, e as SessionEndReason, A as ActivityPolicySlice, f as ActivityListFilter, g as ActivityRecord, h as ActivityTurnSession, i as ActivityPrSession, j as ActivityWorkflowRunSlice, P as PrResolvedState, k as ActivityTaskSlice, W as WorktreeProvisioner, l as AgentsMdResolution, U as UserPreset, R as RestartPolicy, m as SandboxAppServeSpec, n as WorktreeField, o as AgentSessionLike, p as UsageReadContext, q as UsageReadResult, r as MessageKind, s as SessionMessage, t as AgentStreamEvent, u as SessionUsage, v as ToolCallRecord, w as StatsSessionDescriptor, x as ProcRow, y as ProvisionInFlight, z as ProvisionTracker, B as UsageRollup, D as RemainingQuotaReader, Q as QuotaReadableProfile, E as UsageSnapshotRecord, F as SessionAwaitingQuestion, G as PolicyRunState, H as PtyFactory, I as WorktreeAutoReclaimer } from './sessions-DHSLsPHA.js';
7
- export { J as ActivityKind, K as ActivitySource, L as ActivityState, N as ActivityWaitingOn, O as AnthropicReaderConfig, V as AnthropicRemainingQuotaReader, X as AttachPolicyInput, Y as CommandGroupStats, Z as CommitSpec, _ as DEFAULT_WORKTREE_ISOLATION, $ as DEFAULT_WORKTREE_PROVISION_CONCURRENCY, a0 as EagerResumeFailReason, a1 as EagerResumeOutcome, a2 as EagerResumeSkipReason, a3 as GateSpec, a4 as HostInfo, a5 as JudgeGateSpec, a6 as LabeledProcessStatsReport, a7 as LabeledSessionStats, a8 as MAX_MESSAGE_DATA_BYTES, a9 as MESSAGE_KINDS, aa as MESSAGE_PREAMBLE, ab as MESSAGE_TAG, ac as MESSAGE_URGENCIES, ad as MessageDeliveryVia, ae as MessageFilter, af as MessageRelation, ag as MessageTreeNode, ah as OnFailSpec, ai as OrphanGroup, aj as PendingPermission, ak as PermissionRespondInput, al as PermissionRespondResult, am as PolicyRunStatus, an as PricingResolver, ao as ProcessDetail, ap as ProcessStatsReport, aq as ProcessStatsService, ar as QuotaStoreFile, as as QuotaStoreOptions, at as RegisterBrowserInput, au as RegisterSessionInput, av as RemainingQuota, aw as ResourceStats, ax as ResumeDisabledError, ay as SESSION_END_REASONS, az as SESSION_ID_ENV, aA as SessionCurrentPhase, aB as SessionKind, aC as SessionMessageFrom, aD as SessionOutcome, aE as SessionOutcomeArtifact, aF as SessionOutcomeCompact, aG as SessionOutcomeLink, aH as SessionResourceStats, aI as SessionSnapshots, aJ as SessionStatus, aK as ShellGateSpec, aL as SpawnAgentInput, aM as SpawnSessionInput, aN as StatsDetail, aO as StoredProfileQuota, aP as TASK_STATUSES, aQ as TaskCaller, aR as TaskChangedEvent, aS as TaskClaimInput, aT as TaskCreateInput, aU as TaskGateOutcome, aV as TaskGateRunner, aW as TaskLedgerRegistry, aX as TaskLedgerSessionSlice, aY as TaskListFilter, aZ as TaskRecord, a_ as TaskStatus, a$ as TaskUpdateInput, b0 as TaskVerification, b1 as TaskVerifySupervisor, b2 as TaskWriteResult, b3 as TokenPricing, b4 as UsageBucket, b5 as UsageComputeInput, b6 as UsageDetail, b7 as UsageSource, b8 as UserPresetsFile, b9 as WORKSPACE_SLUG_ENV, ba as WORKTREE_ISOLATION_ENV, bb as WORKTREE_PROVISION_CONCURRENCY_ENV, bc as WorktreeDecision, bd as WorktreeProvisionOutcome, be as WorktreeProvisionPhase, bf as WorktreeProvisionProgress, bg as WorktreeProvisionRequest, bh as WorktreeRequest, bi as activityCounts, bj as compactOutcome, bk as createProcessStatsService, bl as createSessionMessage, bm as createSupervisorTaskGateRunner, bn as createTaskLedger, bo as decideWorktreeIsolation, bp as deleteUserPreset, bq as deriveSessionOutcome, br as deriveSessionUsage, bs as escapeHumanPrompt, bt as escapeMessageBody, bu as filterActivities, bv as getProcessStatsService, bw as getUserPreset, bx as isClosedTaskStatus, by as isKnownSessionEndReason, bz as isMessageAllowed, bA as isProviderLimitError, bB as isTerminalActivityState, bC as listUserPresets, bD as loadQuotaStore, bE as loadUserPresets, bF as loadWorktreeIsolation, bG as matchesMessageFilter, bH as messageFrom, bI as mintMessageId, bJ as normalizeCommand, bK as normalizeWorktreeField, bL as parseAnthropicRateLimitHeaders, bM as parseTaskStatus, bN as parseWindow, bO as parseWorktreeIsolationMode, bP as pickUsageDetail, bQ as policyToActivities, bR as policyWatchesSession, bS as prToActivities, bT as projectSessionUsage, bU as readProfileQuota, bV as recordProfileQuota, bW as renderInboxDigest, bX as renderSessionMessage, bY as renderSessionMessages, bZ as resolveRelation, b_ as rollupUsage, b$ as sanitizeLabel, c0 as saveUserPreset, c1 as trackWorktreeProvision, c2 as turnToActivities, c3 as userPresetsPath, c4 as workflowToActivities } from './sessions-DHSLsPHA.js';
6
+ import { M as MessageUrgency, S as SessionEvent, a as SessionEventType, b as SessionEventBus, c as SessionsRegistry, C as CompletionPolicySupervisor, T as TaskLedger, d as SessionDescriptor, e as SessionEndReason, A as ActivityPolicySlice, f as ActivityListFilter, g as ActivityRecord, h as ActivityTurnSession, i as ActivityPrSession, j as ActivityWorkflowRunSlice, P as PrResolvedState, k as ActivityTaskSlice, W as WorktreeProvisioner, l as AgentsMdResolution, U as UserPreset, R as RestartPolicy, m as SandboxAppServeSpec, n as WorktreeField, o as AgentSessionLike, p as UsageReadContext, q as UsageReadResult, r as MessageKind, s as SessionMessage, t as AgentStreamEvent, u as SessionUsage, v as ToolCallRecord, w as StatsSessionDescriptor, x as ProcRow, y as ProvisionInFlight, z as ProvisionTracker, B as UsageRollup, D as RemainingQuotaReader, Q as QuotaReadableProfile, E as UsageSnapshotRecord, F as SessionAwaitingQuestion, G as PolicyRunState, H as PtyFactory, I as WorktreeAutoReclaimer } from './sessions-SrISpFsH.js';
7
+ export { J as ActivityKind, K as ActivitySource, L as ActivityState, N as ActivityWaitingOn, O as AnthropicReaderConfig, V as AnthropicRemainingQuotaReader, X as AttachPolicyInput, Y as CommandGroupStats, Z as CommitSpec, _ as DEFAULT_WORKTREE_ISOLATION, $ as DEFAULT_WORKTREE_PROVISION_CONCURRENCY, a0 as EagerResumeFailReason, a1 as EagerResumeOutcome, a2 as EagerResumeSkipReason, a3 as GateSpec, a4 as HostInfo, a5 as JudgeGateSpec, a6 as LabeledProcessStatsReport, a7 as LabeledSessionStats, a8 as MAX_MESSAGE_DATA_BYTES, a9 as MESSAGE_KINDS, aa as MESSAGE_PREAMBLE, ab as MESSAGE_TAG, ac as MESSAGE_URGENCIES, ad as MessageDeliveryVia, ae as MessageFilter, af as MessageRelation, ag as MessageTreeNode, ah as OnFailSpec, ai as OrphanGroup, aj as PendingPermission, ak as PermissionRespondInput, al as PermissionRespondResult, am as PolicyRunStatus, an as PricingResolver, ao as ProcessDetail, ap as ProcessStatsReport, aq as ProcessStatsService, ar as QuotaStoreFile, as as QuotaStoreOptions, at as RegisterBrowserInput, au as RegisterSessionInput, av as RemainingQuota, aw as ResourceStats, ax as ResumeDisabledError, ay as SESSION_END_REASONS, az as SESSION_ID_ENV, aA as SessionCurrentPhase, aB as SessionKind, aC as SessionMessageFrom, aD as SessionOutcome, aE as SessionOutcomeArtifact, aF as SessionOutcomeCompact, aG as SessionOutcomeLink, aH as SessionResourceStats, aI as SessionSnapshots, aJ as SessionStatus, aK as ShellGateSpec, aL as SpawnAgentInput, aM as SpawnSessionInput, aN as StatsDetail, aO as StoredProfileQuota, aP as TASK_STATUSES, aQ as TaskCaller, aR as TaskChangedEvent, aS as TaskClaimInput, aT as TaskCreateInput, aU as TaskGateOutcome, aV as TaskGateRunner, aW as TaskLedgerRegistry, aX as TaskLedgerSessionSlice, aY as TaskListFilter, aZ as TaskRecord, a_ as TaskStatus, a$ as TaskUpdateInput, b0 as TaskVerification, b1 as TaskVerifySupervisor, b2 as TaskWriteResult, b3 as TokenPricing, b4 as UsageBucket, b5 as UsageComputeInput, b6 as UsageDetail, b7 as UsageSource, b8 as UserPresetsFile, b9 as WORKSPACE_SLUG_ENV, ba as WORKTREE_ISOLATION_ENV, bb as WORKTREE_PROVISION_CONCURRENCY_ENV, bc as WorktreeDecision, bd as WorktreeProvisionOutcome, be as WorktreeProvisionPhase, bf as WorktreeProvisionProgress, bg as WorktreeProvisionRequest, bh as WorktreeRequest, bi as activityCounts, bj as compactOutcome, bk as createProcessStatsService, bl as createSessionMessage, bm as createSupervisorTaskGateRunner, bn as createTaskLedger, bo as decideWorktreeIsolation, bp as deleteUserPreset, bq as deriveSessionOutcome, br as deriveSessionUsage, bs as escapeHumanPrompt, bt as escapeMessageBody, bu as filterActivities, bv as getProcessStatsService, bw as getUserPreset, bx as isClosedTaskStatus, by as isKnownSessionEndReason, bz as isMessageAllowed, bA as isProviderLimitError, bB as isTerminalActivityState, bC as listUserPresets, bD as loadQuotaStore, bE as loadUserPresets, bF as loadWorktreeIsolation, bG as matchesMessageFilter, bH as messageFrom, bI as mintMessageId, bJ as normalizeCommand, bK as normalizeWorktreeField, bL as parseAnthropicRateLimitHeaders, bM as parseTaskStatus, bN as parseWindow, bO as parseWorktreeIsolationMode, bP as pickUsageDetail, bQ as policyToActivities, bR as policyWatchesSession, bS as prToActivities, bT as projectSessionUsage, bU as readProfileQuota, bV as recordProfileQuota, bW as renderInboxDigest, bX as renderSessionMessage, bY as renderSessionMessages, bZ as resolveRelation, b_ as rollupUsage, b$ as sanitizeLabel, c0 as saveUserPreset, c1 as trackWorktreeProvision, c2 as turnToActivities, c3 as userPresetsPath, c4 as workflowToActivities } from './sessions-SrISpFsH.js';
8
8
  import { R as RouteSpec, P as Posture, C as ContextProfile, a as CanonicalPosture } from './session-config-DxIIuJFY.js';
9
9
  export { A as AuthMethod, D as DeclaredAdapterMode, E as EffortLevel, S as SessionConfig, c as composeMode, d as decomposeMode } from './session-config-DxIIuJFY.js';
10
10
  import { AcpMcpServer } from '@agentproto/acp';
@@ -2201,6 +2201,12 @@ interface SpawnAgentSessionDeps {
2201
2201
  * tool and HTTP route each have their own "not enabled" response
2202
2202
  * shape for the missing-resolver case). */
2203
2203
  resolveAgentAdapter: AgentAdapterResolver;
2204
+ /** Installed-adapter lister — ONLY used to enumerate the valid slugs in
2205
+ * the generic `adapter_not_found` error, so a genuinely unknown adapter
2206
+ * name names what IS installed instead of just saying "install it".
2207
+ * Omitted ⇒ the error keeps its previous wording. Mirrors
2208
+ * `CronAdapterCheckDeps.listAgentAdapters` (cron's create-time check). */
2209
+ listAgentAdapters?: AgentAdapterLister;
2204
2210
  /** Optional orchestrator-injection builder (WP3). See
2205
2211
  * `RegisterAgentToolsOptions.buildOrchestratorMcp` for the full
2206
2212
  * contract. Omitted → `orchestrator` is rejected with
@@ -3819,6 +3825,10 @@ type GhRunner$1 = (args: readonly string[], cwd: string) => Promise<{
3819
3825
  * B. BRANCH — resolve the OPEN PR for the session cwd's current branch
3820
3826
  * (via the injected {@link OpenPrResolver} port), the fallback for
3821
3827
  * adapters whose tool calls aren't recorded.
3828
+ * C. TEXTUAL — pull-request urls mentioned in the session's own assistant
3829
+ * output (bounded transcript-tail scan); records on `openedPrs`
3830
+ * (adapter "output-scan") but never stamps a footer — a mention is
3831
+ * not proof of creation.
3822
3832
  *
3823
3833
  * Turn-end/exit are only poll checkpoints, never an assertion the PR is
3824
3834
  * ready: the real predicate is "did this session create a PR / does an open
@@ -3855,6 +3865,10 @@ interface ReconcilerSession extends FooterSession {
3855
3865
  url: string;
3856
3866
  number?: number;
3857
3867
  }[];
3868
+ /** Ephemeral transcript path the live registry stamps on every read
3869
+ * (see `SessionDescriptor.eventsPath`); lane C's bounded tail read
3870
+ * needs it. Absent from test fakes without a transcript on disk. */
3871
+ eventsPath?: string;
3858
3872
  }
3859
3873
  /** The registry slice the reconciler needs — structurally satisfied by the full
3860
3874
  * `SessionsRegistry`, and by a trivial fake in tests. It is also a valid
package/dist/index.mjs CHANGED
@@ -9046,6 +9046,20 @@ var init_mcp_credential_deps = __esm({
9046
9046
  });
9047
9047
 
9048
9048
  // src/sandbox-agent-session-proxy.ts
9049
+ function stripAnsiCodes(s) {
9050
+ return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
9051
+ }
9052
+ async function* harvestHostErrorNotice(host, remoteSessionId) {
9053
+ let tail;
9054
+ try {
9055
+ tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
9056
+ } catch {
9057
+ return;
9058
+ }
9059
+ const lines = tail.split(/\r?\n/).map((l) => stripAnsiCodes(l).trim()).filter((l) => l.length > 0 && HOST_ERROR_LINE_RE.test(l));
9060
+ if (lines.length === 0) return;
9061
+ yield { kind: "notice", text: lines.join("\n") };
9062
+ }
9049
9063
  function extractPromptText(message) {
9050
9064
  if (typeof message === "string") return message;
9051
9065
  if (message && typeof message === "object" && "text" in message) {
@@ -9168,13 +9182,18 @@ function createSandboxAgentSessionProxy(opts) {
9168
9182
  let lastPrompt;
9169
9183
  let closed = false;
9170
9184
  let transcriptCursor = 0;
9185
+ let turnsSent = 0;
9171
9186
  return {
9172
9187
  sessionId: remoteSessionId,
9173
9188
  async *send(message) {
9174
9189
  const prompt = extractPromptText(message);
9175
9190
  lastPrompt = prompt;
9191
+ const isFirstTurn = turnsSent === 0;
9192
+ turnsSent++;
9176
9193
  const since = await host.currentEventsCursor();
9177
- await host.prompt(remoteSessionId, prompt);
9194
+ if (!(opts.promptCarriedInStart && isFirstTurn)) {
9195
+ await host.prompt(remoteSessionId, prompt);
9196
+ }
9178
9197
  const controller = new AbortController();
9179
9198
  let sseBody;
9180
9199
  try {
@@ -9193,15 +9212,23 @@ function createSandboxAgentSessionProxy(opts) {
9193
9212
  }
9194
9213
  try {
9195
9214
  let sawTurnEnd = false;
9215
+ let sawAssistantText = false;
9216
+ let sawToolCall = false;
9196
9217
  for await (const record2 of readSseRecords(sseBody)) {
9197
9218
  if (typeof record2.seq === "number") transcriptCursor = record2.seq;
9198
9219
  const evt = recordToStreamEvent(record2);
9199
9220
  if (!evt) continue;
9200
- yield evt;
9221
+ if (evt.kind === "text-delta" && evt.text?.trim()) sawAssistantText = true;
9222
+ else if (evt.kind === "tool-call") sawToolCall = true;
9201
9223
  if (evt.kind === "turn-end") {
9202
9224
  sawTurnEnd = true;
9225
+ if (opts.surfaceHostTurnErrors && isFirstTurn && (evt.reason === "error" || !sawAssistantText && !sawToolCall)) {
9226
+ yield* harvestHostErrorNotice(host, remoteSessionId);
9227
+ }
9228
+ yield evt;
9203
9229
  break;
9204
9230
  }
9231
+ yield evt;
9205
9232
  }
9206
9233
  if (!sawTurnEnd) {
9207
9234
  throw new Error(
@@ -9249,7 +9276,7 @@ function createSandboxAgentSessionProxy(opts) {
9249
9276
  }
9250
9277
  };
9251
9278
  }
9252
- var MAX_POLL_MS, MAX_OUTPUT_LINES, MAX_CONSECUTIVE_POLL_FAILURES, POLL_RETRY_DELAY_MS, STREAM_EVENT_RECORD_KINDS;
9279
+ var MAX_POLL_MS, MAX_OUTPUT_LINES, MAX_CONSECUTIVE_POLL_FAILURES, POLL_RETRY_DELAY_MS, HOST_ERROR_LINE_RE, STREAM_EVENT_RECORD_KINDS;
9253
9280
  var init_sandbox_agent_session_proxy = __esm({
9254
9281
  "src/sandbox-agent-session-proxy.ts"() {
9255
9282
  init_sandbox_ledger();
@@ -9257,6 +9284,7 @@ var init_sandbox_agent_session_proxy = __esm({
9257
9284
  MAX_OUTPUT_LINES = 500;
9258
9285
  MAX_CONSECUTIVE_POLL_FAILURES = 6;
9259
9286
  POLL_RETRY_DELAY_MS = 5e3;
9287
+ HOST_ERROR_LINE_RE = /\[(?:error|warning)\]|\berror\b|\bwarning\b|invalid model|model not found|unknown model|unsupported model/i;
9260
9288
  STREAM_EVENT_RECORD_KINDS = /* @__PURE__ */ new Set([
9261
9289
  "text-delta",
9262
9290
  "thought",
@@ -10428,6 +10456,7 @@ async function spawnAgentSession(deps2, input) {
10428
10456
  const {
10429
10457
  registry,
10430
10458
  resolveAgentAdapter,
10459
+ listAgentAdapters,
10431
10460
  buildOrchestratorMcp,
10432
10461
  daemonMcpUrl,
10433
10462
  callerScope,
@@ -10914,10 +10943,16 @@ async function spawnAgentSession(deps2, input) {
10914
10943
  if (input.sandbox === void 0 && !resolved) {
10915
10944
  const profileHint = await authProfileAsAdapterHint(input.adapter, getAuthProfile);
10916
10945
  const gatewayHint = profileHint ? void 0 : gatewayAsAdapterHint(input.adapter);
10946
+ let installedHint;
10947
+ if (!profileHint && !gatewayHint) {
10948
+ const installed = await listAgentAdapters?.().catch(() => void 0);
10949
+ installedHint = installed && installed.length > 0 ? `Installed adapters: ${installed.map((a) => a.slug).sort().join(", ")}.` : void 0;
10950
+ }
10951
+ const genericHint = `If it was working a moment ago, something may be mid-rebuild \u2014 wait and retry. ` + (installedHint ?? `If it has never been installed, run \`agentproto install ${input.adapter}\` first.`);
10917
10952
  return {
10918
10953
  ok: false,
10919
10954
  code: "adapter_not_found",
10920
- message: profileHint ? `agent_start: adapter "${input.adapter}" could not be resolved. ${profileHint}` : gatewayHint ? `agent_start: adapter "${input.adapter}" could not be resolved. ${gatewayHint}` : `agent_start: adapter "${input.adapter}" could not be resolved. If it was working a moment ago, something may be mid-rebuild \u2014 wait and retry. If it has never been installed, run \`agentproto install ${input.adapter}\` first.`
10955
+ message: profileHint ? `agent_start: adapter "${input.adapter}" could not be resolved. ${profileHint}` : gatewayHint ? `agent_start: adapter "${input.adapter}" could not be resolved. ${gatewayHint}` : `agent_start: adapter "${input.adapter}" could not be resolved. ${genericHint}`
10921
10956
  };
10922
10957
  }
10923
10958
  if (resolveHostAuth && !resolved) {
@@ -11528,7 +11563,15 @@ ${asyncPrompt}`;
11528
11563
  // (never the value) before the box boots.
11529
11564
  ...authSpec ? { authSpec } : {},
11530
11565
  ...descriptorRoute ? { route: descriptorRoute } : {},
11531
- ...input.appServe ? { appServe: input.appServe } : {}
11566
+ ...input.appServe ? { appServe: input.appServe } : {},
11567
+ // F2 single-dial spawn — DEVICE ONLY: carry the composed initial
11568
+ // prompt INSIDE the inner `agent_start` (see `bootSandboxAgentSession`'s
11569
+ // `initialPrompt`) so the device bridge makes ONE dial, not two. The
11570
+ // controller still drives/records the first turn through the proxy
11571
+ // below (`registry.spawnAgent`'s `initialPrompt` / the `wait`
11572
+ // `sendPrompt`); `promptCarriedInStart` stops the proxy from
11573
+ // re-sending it. Non-device sandboxes are untouched (two-dial shape).
11574
+ ...isDeviceSpawn && effectivePrompt !== void 0 ? { initialPrompt: effectivePrompt } : {}
11532
11575
  });
11533
11576
  if (!booted.ok) return booted;
11534
11577
  agentSession = booted.agentSession;
@@ -11902,6 +11945,29 @@ function withSandboxInstallAdapters(spec) {
11902
11945
  function looksLikeHostOnlyPath(p) {
11903
11946
  return HOST_ONLY_PATH_PATTERNS.some((re) => re.test(p));
11904
11947
  }
11948
+ function isDeviceTransportError(err) {
11949
+ return DEVICE_TRANSPORT_ERROR_RE.test(err instanceof Error ? err.message : String(err));
11950
+ }
11951
+ function deviceTargetOf(sandbox) {
11952
+ const slug = typeof sandbox === "string" ? sandbox : sandbox.provider;
11953
+ return slug.startsWith("device:") ? slug.slice("device:".length) : void 0;
11954
+ }
11955
+ async function startDeviceSessionWithRetry(host, args, target) {
11956
+ const attempts = 2;
11957
+ let lastErr;
11958
+ for (let attempt = 1; attempt <= attempts; attempt++) {
11959
+ try {
11960
+ return await host.start(args);
11961
+ } catch (err) {
11962
+ lastErr = err;
11963
+ if (!isDeviceTransportError(err)) throw err;
11964
+ if (attempt < attempts) {
11965
+ await new Promise((resolve42) => setTimeout(resolve42, DEVICE_SPAWN_RETRY_DELAY_MS));
11966
+ }
11967
+ }
11968
+ }
11969
+ throw new DeviceSpawnUnreachableError(target, attempts, lastErr);
11970
+ }
11905
11971
  async function bootSandboxAgentSession(opts) {
11906
11972
  const providerSlug = typeof opts.sandbox === "string" ? opts.sandbox : opts.sandbox.provider;
11907
11973
  if (!opts.resolveSandboxProvider) {
@@ -12014,10 +12080,12 @@ async function bootSandboxAgentSession(opts) {
12014
12080
  ...lifecyclePolicy.pauseAfterIdleMs !== void 0 ? { expiresAt: new Date(Date.now() + lifecyclePolicy.pauseAfterIdleMs).toISOString() } : {}
12015
12081
  });
12016
12082
  const omitCwd = handle.omitCwdWhenImplicit === true && !opts.explicitCwd;
12083
+ const deviceTarget = deviceTargetOf(opts.sandbox);
12084
+ const carriedPrompt = deviceTarget !== void 0 && opts.initialPrompt !== void 0 ? typeof opts.initialPrompt === "string" ? [{ type: "text", text: opts.initialPrompt }] : opts.initialPrompt : void 0;
12017
12085
  let remoteSessionId;
12018
12086
  let resolvedCwd = boxCwd;
12019
12087
  try {
12020
- const remoteDesc = await host.start({
12088
+ const startArgs = {
12021
12089
  adapter: opts.adapter,
12022
12090
  ...omitCwd ? {} : { cwd: boxCwd },
12023
12091
  // Issue #1647 — DEVICE-BRIDGE ONLY: forward an EXPLICIT
@@ -12035,13 +12103,24 @@ async function bootSandboxAgentSession(opts) {
12035
12103
  ...opts.route ? { route: opts.route } : {},
12036
12104
  ...opts.effort ? { effort: opts.effort } : {},
12037
12105
  ...opts.label ? { label: opts.label } : {},
12038
- ...opts.auth ? { auth: opts.auth } : {}
12039
- });
12106
+ ...opts.auth ? { auth: opts.auth } : {},
12107
+ // F2 — the initial prompt rides the SAME dial as the spawn (device only).
12108
+ ...carriedPrompt !== void 0 ? { prompt: carriedPrompt } : {}
12109
+ };
12110
+ const remoteDesc = deviceTarget !== void 0 ? await startDeviceSessionWithRetry(host, startArgs, deviceTarget) : await host.start(startArgs);
12040
12111
  remoteSessionId = remoteDesc.id;
12041
12112
  if (omitCwd && remoteDesc.cwd) resolvedCwd = remoteDesc.cwd;
12042
12113
  } catch (err) {
12043
12114
  await host.stop().catch(() => void 0);
12044
12115
  recordSandboxState(host.sandboxId, "stopped");
12116
+ if (err instanceof DeviceSpawnUnreachableError) {
12117
+ return {
12118
+ ok: false,
12119
+ code: "device_spawn_unreachable",
12120
+ message: err.message,
12121
+ details: { target: err.target, attempts: err.attempts }
12122
+ };
12123
+ }
12045
12124
  return {
12046
12125
  ok: false,
12047
12126
  code: "sandbox_proxy_failed",
@@ -12068,7 +12147,13 @@ async function bootSandboxAgentSession(opts) {
12068
12147
  host,
12069
12148
  remoteSessionId,
12070
12149
  lifecyclePolicy,
12071
- ledger: { sandboxId: host.sandboxId, provider: providerSlug }
12150
+ ledger: { sandboxId: host.sandboxId, provider: providerSlug },
12151
+ // Device spawns only: surface a first-turn failure's raw host line
12152
+ // (see `surfaceHostTurnErrors`).
12153
+ ...deviceTarget !== void 0 ? { surfaceHostTurnErrors: true } : {},
12154
+ // F2 — when the first prompt rode the spawn dial, don't re-send it as a
12155
+ // second `agent_prompt` dial (see `promptCarriedInStart`).
12156
+ ...carriedPrompt !== void 0 ? { promptCarriedInStart: true } : {}
12072
12157
  }),
12073
12158
  commandPreview: `sandbox:${providerSlug} \u2192 ${opts.adapter}`,
12074
12159
  sandboxId: host.sandboxId,
@@ -12148,7 +12233,7 @@ async function resolveEffectiveCommandSandbox(requested, cwd) {
12148
12233
  return void 0;
12149
12234
  }
12150
12235
  }
12151
- var SPAWN_CLAIM_WINDOW_MS, IMPLICIT_SPAWN_CLAIM_WINDOW_MS, MAX_RESOLVED_CLAIMS, spawnClaimsByRegistry, HOST_ONLY_PATH_PATTERNS;
12236
+ var SPAWN_CLAIM_WINDOW_MS, IMPLICIT_SPAWN_CLAIM_WINDOW_MS, MAX_RESOLVED_CLAIMS, spawnClaimsByRegistry, HOST_ONLY_PATH_PATTERNS, DEVICE_TRANSPORT_ERROR_RE, DEVICE_SPAWN_RETRY_DELAY_MS, DeviceSpawnUnreachableError;
12152
12237
  var init_session_spawn = __esm({
12153
12238
  "src/session-spawn.ts"() {
12154
12239
  init_process_stats();
@@ -12197,6 +12282,21 @@ var init_session_spawn = __esm({
12197
12282
  /^\/Users\//,
12198
12283
  /^[A-Za-z]:[\\/]/
12199
12284
  ];
12285
+ DEVICE_TRANSPORT_ERROR_RE = /device_unreachable|transport closed|handshake timed out|did not send hello|closed the channel after handshake|socket hang up|other side closed|ECONNRESET|ECONNREFUSED|EPIPE|ETIMEDOUT|fetch failed/i;
12286
+ DEVICE_SPAWN_RETRY_DELAY_MS = 2e3;
12287
+ DeviceSpawnUnreachableError = class extends Error {
12288
+ target;
12289
+ attempts;
12290
+ constructor(target, attempts, cause) {
12291
+ const detail = cause instanceof Error ? cause.message : String(cause);
12292
+ super(
12293
+ `agent_start: could not reach paired device "${target}" over its host channel after ${attempts} attempt${attempts === 1 ? "" : "s"} \u2014 the host channel may be flapping; retry in a few seconds and check \`agentproto devices status ${target}\`. Last error: ${detail}`
12294
+ );
12295
+ this.name = "DeviceSpawnUnreachableError";
12296
+ this.target = target;
12297
+ this.attempts = attempts;
12298
+ }
12299
+ };
12200
12300
  }
12201
12301
  });
12202
12302
  function tryParseJson(value) {
@@ -13667,7 +13767,10 @@ function registerAgentTools(server, opts) {
13667
13767
  ...provisionWorktree ? { provisionWorktree } : {},
13668
13768
  ...resolveWorktreeIsolation ? { resolveWorktreeIsolation } : {},
13669
13769
  ...listCatalogModels ? { listCatalogModels } : {},
13670
- ...ensureLlmEndpointRunning ? { ensureLlmEndpointRunning } : {}
13770
+ ...ensureLlmEndpointRunning ? { ensureLlmEndpointRunning } : {},
13771
+ // Lets the generic `adapter_not_found` error name the installed
13772
+ // adapters (session-spawn.ts) instead of only saying "install it".
13773
+ ...listAgentAdapters ? { listAgentAdapters } : {}
13671
13774
  },
13672
13775
  {
13673
13776
  ...spawnInput,
@@ -13973,6 +14076,11 @@ function registerAgentTools(server, opts) {
13973
14076
  // Surfaced so a caller can distinguish "idle" from "mid tool
13974
14077
  // call" without guessing from empty output.
13975
14078
  ...desc.blockedOn ? { blockedOn: desc.blockedOn } : {},
14079
+ // Device-spawn first-turn failure (bad model id / host-side
14080
+ // adapter error) — see `SessionDescriptor.firstTurnFailed`.
14081
+ // The underlying host error line rides the `lines` tail above
14082
+ // (surfaced as a `notice` by the sandbox proxy).
14083
+ ...desc.firstTurnFailed ? { firstTurnFailed: true } : {},
13976
14084
  ...activityFallback ? { activityFallback: true } : {},
13977
14085
  // An ended session's derived outcome (what it produced) —
13978
14086
  // the ring above is empty for a row reloaded after a restart.
@@ -14209,6 +14317,7 @@ function registerAgentTools(server, opts) {
14209
14317
  ...s.lastTurnErrorMessage !== void 0 ? { lastTurnErrorMessage: s.lastTurnErrorMessage } : {},
14210
14318
  ...s.lastTurnReason !== void 0 ? { lastTurnReason: s.lastTurnReason } : {},
14211
14319
  ...s.lastTurnEmpty !== void 0 ? { lastTurnEmpty: s.lastTurnEmpty } : {},
14320
+ ...s.firstTurnFailed ? { firstTurnFailed: true } : {},
14212
14321
  ...s.stats !== void 0 ? { stats: s.stats } : {}
14213
14322
  });
14214
14323
  const agentSessionsListSchema = z.object({
@@ -16872,18 +16981,18 @@ function worktreeFields(cwd) {
16872
16981
  ...identity.mainRepoPath === void 0 ? {} : { mainRepoPath: identity.mainRepoPath }
16873
16982
  };
16874
16983
  }
16875
- function stripAnsiCodes(s) {
16984
+ function stripAnsiCodes2(s) {
16876
16985
  return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
16877
16986
  }
16878
16987
  function terminalOutputTail(rt, maxChars = 500) {
16879
16988
  if (rt.recentBytes.length === 0) return "";
16880
16989
  const text13 = Buffer.concat(rt.recentBytes, rt.recentBytesSize).toString("utf8");
16881
- const plain = stripAnsiCodes(text13).replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b[()][A-Za-z0-9]/g, "").replace(/\x1b[0-9A-Za-z=>]/g, "").replace(/[\x00-\x09\x0b-\x1f]/g, "").trim();
16990
+ const plain = stripAnsiCodes2(text13).replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b[()][A-Za-z0-9]/g, "").replace(/\x1b[0-9A-Za-z=>]/g, "").replace(/[\x00-\x09\x0b-\x1f]/g, "").trim();
16882
16991
  if (!plain) return "";
16883
16992
  return plain.length > maxChars ? plain.slice(-maxChars) : plain;
16884
16993
  }
16885
16994
  function deriveHeuristicQuestion(recentLines) {
16886
- const visible = recentLines.map(stripAnsiCodes).map((l) => l.trim()).filter((l) => l.length > 0 && !SYNTHETIC_LINE.test(l));
16995
+ const visible = recentLines.map(stripAnsiCodes2).map((l) => l.trim()).filter((l) => l.length > 0 && !SYNTHETIC_LINE.test(l));
16887
16996
  if (visible.length === 0) return void 0;
16888
16997
  const options = [];
16889
16998
  let i = visible.length - 1;
@@ -17481,7 +17590,7 @@ function createSessionsRegistry(opts) {
17481
17590
  const strategySlug = rt.desc.adapterSlug ?? conversationTerminalSlugFor(rt.desc);
17482
17591
  const strategy = strategySlug ? RESUME_STRATEGIES[strategySlug] : void 0;
17483
17592
  if (!strategy?.outputHint) return;
17484
- const plain = stripAnsiCodes(line);
17593
+ const plain = stripAnsiCodes2(line);
17485
17594
  const m = plain.match(strategy.outputHint);
17486
17595
  if (m && m[1]) {
17487
17596
  rt.desc.resumeMetadata = {
@@ -17719,6 +17828,14 @@ function createSessionsRegistry(opts) {
17719
17828
  }
17720
17829
  break;
17721
17830
  }
17831
+ case "notice": {
17832
+ if (evt.text) {
17833
+ for (const line of evt.text.split(/\r?\n/)) {
17834
+ if (line.trim()) appendLine(rt, `\x1B[2m[notice] ${line}\x1B[0m`, "stderr");
17835
+ }
17836
+ }
17837
+ break;
17838
+ }
17722
17839
  case "plan": {
17723
17840
  const entries = evt.entries ?? [];
17724
17841
  const done = entries.filter((e) => e.status === "completed").length;
@@ -18638,6 +18755,10 @@ ${message}`;
18638
18755
  } else if (rt.desc.lastTurnReason !== void 0) {
18639
18756
  delete rt.desc.lastTurnReason;
18640
18757
  }
18758
+ if (rt.desc.remote === true && rt.desc.sandboxProvider?.startsWith("device:") && rt.desc.turnsCompleted === 1 && (emptyTurn || turnEndReason === "error")) {
18759
+ rt.desc.firstTurnFailed = true;
18760
+ schedulePersist();
18761
+ }
18641
18762
  const nextActivity = regenerateActivitySummary(rt.desc, rt.recentLines, Date.now());
18642
18763
  if (nextActivity) {
18643
18764
  rt.desc.activitySummary = nextActivity;
@@ -18692,6 +18813,10 @@ ${message}`;
18692
18813
  } else if (rt.desc.lastTurnReason !== void 0) {
18693
18814
  delete rt.desc.lastTurnReason;
18694
18815
  }
18816
+ if (rt.desc.remote === true && rt.desc.sandboxProvider?.startsWith("device:") && rt.desc.turnsCompleted === 1 && turnEndReason === "error") {
18817
+ rt.desc.firstTurnFailed = true;
18818
+ schedulePersist();
18819
+ }
18695
18820
  if (sessionEvents) {
18696
18821
  sessionEvents.emit({
18697
18822
  type: "session:turn-end",
@@ -25995,6 +26120,7 @@ var compactSessionItem = (s) => ({
25995
26120
  ...s.lastTurnErrorMessage !== void 0 ? { lastTurnErrorMessage: s.lastTurnErrorMessage } : {},
25996
26121
  ...s.lastTurnReason !== void 0 ? { lastTurnReason: s.lastTurnReason } : {},
25997
26122
  ...s.lastTurnEmpty !== void 0 ? { lastTurnEmpty: s.lastTurnEmpty } : {},
26123
+ ...s.firstTurnFailed ? { firstTurnFailed: true } : {},
25998
26124
  ...s.interrupted ? { interrupted: true } : {},
25999
26125
  ...s.rssBytes !== void 0 ? { rssBytes: s.rssBytes } : {},
26000
26126
  ...s.stats !== void 0 ? { stats: s.stats } : {},
@@ -31672,12 +31798,18 @@ function registerMcpApps(server, apps) {
31672
31798
  uri: resourceUri,
31673
31799
  html,
31674
31800
  description: app.description,
31675
- csp: app.csp
31801
+ csp: app.csp,
31802
+ ...app.openai?.resource !== void 0 ? {
31803
+ // Namespaced under `openai/ui` per §3.2 — a sibling of the
31804
+ // canonical `_meta.ui`, never an overwrite of it.
31805
+ meta: { "openai/ui": JSON.parse(JSON.stringify(app.openai.resource)) }
31806
+ } : {}
31676
31807
  });
31677
31808
  server.registerTool(
31678
31809
  app.id,
31679
31810
  {
31680
31811
  description: app.description ?? app.title,
31812
+ ...app.openai !== void 0 ? { title: app.title } : {},
31681
31813
  inputSchema: app.inputSchema.shape,
31682
31814
  annotations: {
31683
31815
  readOnlyHint: true,
@@ -31687,7 +31819,13 @@ function registerMcpApps(server, apps) {
31687
31819
  ui: {
31688
31820
  resourceUri,
31689
31821
  visibility: ["model", "app"]
31690
- }
31822
+ },
31823
+ ...app.openai?.tool?.entrypoints !== void 0 || app.openai?.icons !== void 0 ? {
31824
+ "openai/ui": {
31825
+ ...app.openai.tool?.entrypoints !== void 0 ? { entrypoints: JSON.parse(JSON.stringify(app.openai.tool.entrypoints)) } : {},
31826
+ ...app.openai.icons !== void 0 ? { icons: JSON.parse(JSON.stringify(app.openai.icons)) } : {}
31827
+ }
31828
+ } : {}
31691
31829
  }
31692
31830
  },
31693
31831
  async (args) => {
@@ -43534,7 +43672,15 @@ async function performInstall(dir, appRegistry, listRegisteredToolIds, resolveAg
43534
43672
  ...handle.ui?.description !== void 0 ? { description: handle.ui.description } : {},
43535
43673
  ...handle.ui?.tools !== void 0 ? { tools: handle.ui.tools } : {},
43536
43674
  ...handle.ui?.csp !== void 0 ? { csp: handle.ui.csp } : {},
43537
- ...handle.ui?.build !== void 0 ? { build: handle.ui.build } : {}
43675
+ ...handle.ui?.build !== void 0 ? { build: handle.ui.build } : {},
43676
+ // OpenAI MCP-extensions carrier (plan W-B): the normalized,
43677
+ // app-kit-validated `ui.extensions` block persists verbatim on the
43678
+ // installed record — `makeInstalledAppUiApps` (W-C) is the only
43679
+ // consumer. Frozen already by app-kit; spread-cloned here so the
43680
+ // persisted shape stays a plain JSON-safe object (apps.json is
43681
+ // what actually freezes it). Apps without the block install
43682
+ // identically to before (plan I1).
43683
+ ...handle.ui?.extensions !== void 0 ? { extensions: handle.ui.extensions } : {}
43538
43684
  } : void 0;
43539
43685
  const artifact = refs.artifact ? {
43540
43686
  path: refs.artifact.path,
@@ -46565,6 +46711,7 @@ async function startHttpServer(opts) {
46565
46711
  opts.resolveSandboxProvider,
46566
46712
  opts.webhookNotifier,
46567
46713
  opts.ensureLlmEndpointRunning,
46714
+ opts.listAgentAdapters,
46568
46715
  opts.deviceMirrorSync
46569
46716
  );
46570
46717
  if (handled) return;
@@ -48617,7 +48764,7 @@ async function handleSandboxes(_req, res, path, resolveSandboxProvider2) {
48617
48764
  json3(200, { alive: true, ...probe.state ? { state: probe.state } : { state: row.state }, checkedAt });
48618
48765
  return true;
48619
48766
  }
48620
- async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false, resolveBrowserAdapter, listBrowserAdapters, sessionEvents, eventRing, buildOrchestratorMcp, daemonMcpUrl, provisionWorktree, listCatalogModels, resolveSandboxProvider2, webhookNotifier, ensureLlmEndpointRunning, deviceMirrorSync) {
48767
+ async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false, resolveBrowserAdapter, listBrowserAdapters, sessionEvents, eventRing, buildOrchestratorMcp, daemonMcpUrl, provisionWorktree, listCatalogModels, resolveSandboxProvider2, webhookNotifier, ensureLlmEndpointRunning, listAgentAdapters, deviceMirrorSync) {
48621
48768
  const json3 = (status, body) => {
48622
48769
  res.writeHead(status, { "content-type": "application/json" });
48623
48770
  res.end(JSON.stringify(body));
@@ -48775,12 +48922,13 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
48775
48922
  ...listCatalogModels ? { listCatalogModels } : {},
48776
48923
  ...resolveSandboxProvider2 ? { resolveSandboxProvider: resolveSandboxProvider2 } : {},
48777
48924
  ...webhookNotifier ? { webhookNotifier } : {},
48778
- ...ensureLlmEndpointRunning ? { ensureLlmEndpointRunning } : {}
48925
+ ...ensureLlmEndpointRunning ? { ensureLlmEndpointRunning } : {},
48926
+ ...listAgentAdapters ? { listAgentAdapters } : {}
48779
48927
  },
48780
48928
  spawnArgs
48781
48929
  );
48782
48930
  if (!result.ok) {
48783
- const status = result.code === "adapter_not_found" || result.code === "no_cwd" ? 404 : result.code === "orchestrator_not_enabled" ? 501 : result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? 409 : result.code === "invalid_role" || result.code === "browser_unsupported" || result.code === "worktree_requires_explicit_repo" || result.code === "device_spawn_requires_repo_identity" || result.code === "device_bridge_workspace_unknown" || result.code === "access_profile_not_found" || result.code === "access_profile_ineligible" || result.code === "sandbox_cwd_invalid" ? 400 : 500;
48931
+ const status = result.code === "adapter_not_found" || result.code === "no_cwd" ? 404 : result.code === "orchestrator_not_enabled" ? 501 : result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? 409 : result.code === "invalid_role" || result.code === "browser_unsupported" || result.code === "worktree_requires_explicit_repo" || result.code === "device_spawn_requires_repo_identity" || result.code === "device_bridge_workspace_unknown" || result.code === "access_profile_not_found" || result.code === "access_profile_ineligible" || result.code === "sandbox_cwd_invalid" || result.code === "device_spawn_unreachable" ? 400 : 500;
48784
48932
  json3(status, {
48785
48933
  error: result.code,
48786
48934
  message: result.message,
@@ -48842,12 +48990,13 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
48842
48990
  ...listCatalogModels ? { listCatalogModels } : {},
48843
48991
  ...resolveSandboxProvider2 ? { resolveSandboxProvider: resolveSandboxProvider2 } : {},
48844
48992
  ...webhookNotifier ? { webhookNotifier } : {},
48845
- ...ensureLlmEndpointRunning ? { ensureLlmEndpointRunning } : {}
48993
+ ...ensureLlmEndpointRunning ? { ensureLlmEndpointRunning } : {},
48994
+ ...listAgentAdapters ? { listAgentAdapters } : {}
48846
48995
  },
48847
48996
  spawnArgs
48848
48997
  );
48849
48998
  if (!result.ok) {
48850
- const status = result.code === "adapter_not_found" || result.code === "no_cwd" ? 404 : result.code === "orchestrator_not_enabled" ? 501 : result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? 409 : result.code === "invalid_role" || result.code === "browser_unsupported" || result.code === "worktree_requires_explicit_repo" || result.code === "device_spawn_requires_repo_identity" || result.code === "device_bridge_workspace_unknown" || result.code === "access_profile_not_found" || result.code === "access_profile_ineligible" || result.code === "sandbox_cwd_invalid" ? 400 : 500;
48999
+ const status = result.code === "adapter_not_found" || result.code === "no_cwd" ? 404 : result.code === "orchestrator_not_enabled" ? 501 : result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? 409 : result.code === "invalid_role" || result.code === "browser_unsupported" || result.code === "worktree_requires_explicit_repo" || result.code === "device_spawn_requires_repo_identity" || result.code === "device_bridge_workspace_unknown" || result.code === "access_profile_not_found" || result.code === "access_profile_ineligible" || result.code === "sandbox_cwd_invalid" || result.code === "device_spawn_unreachable" ? 400 : 500;
48851
49000
  json3(status, {
48852
49001
  error: result.code,
48853
49002
  message: result.message,
@@ -57218,6 +57367,7 @@ function createPrProvenanceReconciler(opts) {
57218
57367
  const stampedPrUrls = /* @__PURE__ */ new Set();
57219
57368
  const costRefreshedPrUrls = /* @__PURE__ */ new Set();
57220
57369
  const lastPollAt = /* @__PURE__ */ new Map();
57370
+ const scannedPrUrls = /* @__PURE__ */ new Set();
57221
57371
  const reconcile = async (sessionId, terminal) => {
57222
57372
  const desc = opts.registry.get(sessionId);
57223
57373
  if (!desc || desc.kind !== "agent-cli") return;
@@ -57256,6 +57406,29 @@ function createPrProvenanceReconciler(opts) {
57256
57406
  if (!shouldStamp(record2.createdPrUrl)) continue;
57257
57407
  await stamp({ number: record2.createdPrNumber, url: record2.createdPrUrl });
57258
57408
  }
57409
+ if (desc.eventsPath) {
57410
+ for (const turn of readRecentTurnsSync(desc.eventsPath)) {
57411
+ if (turn.role !== "assistant") continue;
57412
+ const re = /https?:\/\/[^\s<>"]+/g;
57413
+ let m;
57414
+ while ((m = re.exec(turn.text)) !== null) {
57415
+ const url = m[0].replace(/[.,;:)\]]+$/, "");
57416
+ const parsed = parsePrUrl(url);
57417
+ if (!parsed) continue;
57418
+ if (desc.openedPrs?.some((known) => known.url === url)) continue;
57419
+ if (scannedPrUrls.has(url)) continue;
57420
+ scannedPrUrls.add(url);
57421
+ try {
57422
+ opts.registry.recordOpenedPr(sessionId, {
57423
+ adapter: "output-scan",
57424
+ number: parsed.number,
57425
+ url
57426
+ });
57427
+ } catch {
57428
+ }
57429
+ }
57430
+ }
57431
+ }
57259
57432
  const pr = await opts.resolveOpenPr(cwd);
57260
57433
  if (pr && shouldStamp(pr.url)) await stamp(pr);
57261
57434
  await refreshCost(desc, supervisor, cwd);