agent-relay-server 0.121.5 → 0.122.1

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.
Files changed (179) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +6 -6
  3. package/public/assets/{MessageBubble-CLqxCG9b.js → MessageBubble-DRdUD-kW.js} +2 -2
  4. package/public/assets/{MessageBubble-CLqxCG9b.js.map → MessageBubble-DRdUD-kW.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-BkkToFEb.js → PlanGraphPanel-siKMPCln.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-BkkToFEb.js.map → PlanGraphPanel-siKMPCln.js.map} +1 -1
  7. package/public/assets/{activity-D-ocGQGs.js → activity-D3MNcSaV.js} +2 -2
  8. package/public/assets/{activity-D-ocGQGs.js.map → activity-D3MNcSaV.js.map} +1 -1
  9. package/public/assets/{agent-profiles-CRPgYTyb.js → agent-profiles-CKzttq6G.js} +2 -2
  10. package/public/assets/{agent-profiles-CRPgYTyb.js.map → agent-profiles-CKzttq6G.js.map} +1 -1
  11. package/public/assets/{agent-type-icon-CeEoMouY.js → agent-type-icon-0ZJRP1Vy.js} +2 -2
  12. package/public/assets/{agent-type-icon-CeEoMouY.js.map → agent-type-icon-0ZJRP1Vy.js.map} +1 -1
  13. package/public/assets/{agents-Chw5E_2q.js → agents-CuVj_rfG.js} +2 -2
  14. package/public/assets/{agents-Chw5E_2q.js.map → agents-CuVj_rfG.js.map} +1 -1
  15. package/public/assets/{analytics-BmNDG0hR.js → analytics-iob6C27d.js} +2 -2
  16. package/public/assets/{analytics-BmNDG0hR.js.map → analytics-iob6C27d.js.map} +1 -1
  17. package/public/assets/{automation-D7g90kTv.js → automation-B2ixxs4q.js} +2 -2
  18. package/public/assets/{automation-D7g90kTv.js.map → automation-B2ixxs4q.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-Bv7DhLBZ.js → branch-state-badge-CafBJWKo.js} +2 -2
  20. package/public/assets/{branch-state-badge-Bv7DhLBZ.js.map → branch-state-badge-CafBJWKo.js.map} +1 -1
  21. package/public/assets/{channels-CLwPeEPi.js → channels-C-WsbL2H.js} +2 -2
  22. package/public/assets/{channels-CLwPeEPi.js.map → channels-C-WsbL2H.js.map} +1 -1
  23. package/public/assets/{chat-Cgj7VKzc.js → chat-DeoVTVob.js} +2 -2
  24. package/public/assets/{chat-Cgj7VKzc.js.map → chat-DeoVTVob.js.map} +1 -1
  25. package/public/assets/{connectors-Br6fiTny.js → connectors-C2Ac03LJ.js} +2 -2
  26. package/public/assets/{connectors-Br6fiTny.js.map → connectors-C2Ac03LJ.js.map} +1 -1
  27. package/public/assets/display-CJzSoTMq.js +3 -0
  28. package/public/assets/display-CJzSoTMq.js.map +1 -0
  29. package/public/assets/{formatted-body-CmkuD23M.js → formatted-body-35XBOY5L.js} +3 -3
  30. package/public/assets/{formatted-body-CmkuD23M.js.map → formatted-body-35XBOY5L.js.map} +1 -1
  31. package/public/assets/{formatted-body-impl-DbdFmbwW.js → formatted-body-impl-RmDxI5Ux.js} +2 -2
  32. package/public/assets/{formatted-body-impl-DbdFmbwW.js.map → formatted-body-impl-RmDxI5Ux.js.map} +1 -1
  33. package/public/assets/index-D0xMQQb_.js +25 -0
  34. package/public/assets/{index-D9OogaB5.js.map → index-D0xMQQb_.js.map} +1 -1
  35. package/public/assets/{insights-qvaPqWCV.js → insights-B643SFvm.js} +2 -2
  36. package/public/assets/{insights-qvaPqWCV.js.map → insights-B643SFvm.js.map} +1 -1
  37. package/public/assets/{integrations-DYEGSqq_.js → integrations-hFIHeCF4.js} +2 -2
  38. package/public/assets/{integrations-DYEGSqq_.js.map → integrations-hFIHeCF4.js.map} +1 -1
  39. package/public/assets/{maintenance-C0HIEB-J.js → maintenance-CURvdp_Z.js} +2 -2
  40. package/public/assets/{maintenance-C0HIEB-J.js.map → maintenance-CURvdp_Z.js.map} +1 -1
  41. package/public/assets/{managed-agents-DdS7aI38.js → managed-agents-DrZtdlSn.js} +2 -2
  42. package/public/assets/{managed-agents-DdS7aI38.js.map → managed-agents-DrZtdlSn.js.map} +1 -1
  43. package/public/assets/{markdown-preview-impl-BnXMH1z1.js → markdown-preview-impl-CVDO--ek.js} +2 -2
  44. package/public/assets/{markdown-preview-impl-BnXMH1z1.js.map → markdown-preview-impl-CVDO--ek.js.map} +1 -1
  45. package/public/assets/{memory-CSwx7bz8.js → memory-Bm7dt_Qf.js} +2 -2
  46. package/public/assets/{memory-CSwx7bz8.js.map → memory-Bm7dt_Qf.js.map} +1 -1
  47. package/public/assets/{messages-Be9CmvDv.js → messages-DTq4i2KZ.js} +2 -2
  48. package/public/assets/{messages-Be9CmvDv.js.map → messages-DTq4i2KZ.js.map} +1 -1
  49. package/public/assets/{orchestrators-BgtvvHvo.js → orchestrators-Dra318Ap.js} +2 -2
  50. package/public/assets/{orchestrators-BgtvvHvo.js.map → orchestrators-Dra318Ap.js.map} +1 -1
  51. package/public/assets/{overview-BvhbPWtF.js → overview-yr1Vmk64.js} +2 -2
  52. package/public/assets/{overview-BvhbPWtF.js.map → overview-yr1Vmk64.js.map} +1 -1
  53. package/public/assets/{pairs-CpXSOMx0.js → pairs-mnlKDBGp.js} +2 -2
  54. package/public/assets/{pairs-CpXSOMx0.js.map → pairs-mnlKDBGp.js.map} +1 -1
  55. package/public/assets/projects-Dzdf-gkK.js +2 -0
  56. package/public/assets/{projects-DdPNPFJI.js.map → projects-Dzdf-gkK.js.map} +1 -1
  57. package/public/assets/prompt-settings-ByL8SNCi.js +2 -0
  58. package/public/assets/{prompt-settings-BpZse7-X.js.map → prompt-settings-ByL8SNCi.js.map} +1 -1
  59. package/public/assets/{registry-pb1gTZEg.js → registry-CXWyOtpr.js} +2 -2
  60. package/public/assets/{registry-pb1gTZEg.js.map → registry-CXWyOtpr.js.map} +1 -1
  61. package/public/assets/{security-BgcX1n9D.js → security-0SBsc_4W.js} +2 -2
  62. package/public/assets/{security-BgcX1n9D.js.map → security-0SBsc_4W.js.map} +1 -1
  63. package/public/assets/{select-DZPVpKPv.js → select-Bw5z2KJ_.js} +2 -2
  64. package/public/assets/{select-DZPVpKPv.js.map → select-Bw5z2KJ_.js.map} +1 -1
  65. package/public/assets/{settings-hd5kzdRB.js → settings-78Cyq_tA.js} +4 -4
  66. package/public/assets/{settings-hd5kzdRB.js.map → settings-78Cyq_tA.js.map} +1 -1
  67. package/public/assets/{store-DKTLubBT.js → store-BPrVuKiv.js} +4 -4
  68. package/public/assets/store-BPrVuKiv.js.map +1 -0
  69. package/public/assets/{tasks-C5d2LU5E.js → tasks-Cg2MMeri.js} +2 -2
  70. package/public/assets/{tasks-C5d2LU5E.js.map → tasks-Cg2MMeri.js.map} +1 -1
  71. package/public/assets/teams-DWv6UwFV.js +2 -0
  72. package/public/assets/{teams-B8f2pGJe.js.map → teams-DWv6UwFV.js.map} +1 -1
  73. package/public/assets/{terminal-viewer-impl-CxAscH0U.js → terminal-viewer-impl-DBLDjbIg.js} +2 -2
  74. package/public/assets/{terminal-viewer-impl-CxAscH0U.js.map → terminal-viewer-impl-DBLDjbIg.js.map} +1 -1
  75. package/public/assets/{types-uVOXgvMT.js → types-BeJUSfDj.js} +2 -2
  76. package/public/assets/types-BeJUSfDj.js.map +1 -0
  77. package/public/assets/{user-avatar-Dw6mzWhv.js → user-avatar-BsOLyVJj.js} +2 -2
  78. package/public/assets/{user-avatar-Dw6mzWhv.js.map → user-avatar-BsOLyVJj.js.map} +1 -1
  79. package/public/assets/{work-queue-9imc7aNK.js → work-queue-DmwvnF6F.js} +2 -2
  80. package/public/assets/{work-queue-9imc7aNK.js.map → work-queue-DmwvnF6F.js.map} +1 -1
  81. package/public/assets/{workspaces-CLYc3yF3.js → workspaces-dIBivJN8.js} +2 -2
  82. package/public/assets/{workspaces-CLYc3yF3.js.map → workspaces-dIBivJN8.js.map} +1 -1
  83. package/public/index.html +6 -6
  84. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  85. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +44 -938
  86. package/runner/src/adapter.ts +53 -18
  87. package/runner/src/adapters/claude-delivery.ts +127 -0
  88. package/runner/src/adapters/claude-quota-harvest.ts +92 -0
  89. package/runner/src/adapters/claude-session-probe.ts +225 -0
  90. package/runner/src/adapters/claude-status-detectors.ts +147 -0
  91. package/runner/src/adapters/claude-transcript-tail.ts +128 -0
  92. package/runner/src/adapters/claude-transcript.ts +414 -0
  93. package/runner/src/adapters/claude.ts +672 -0
  94. package/runner/src/adapters/codex-agent-message-capture.ts +139 -0
  95. package/runner/src/adapters/codex-client.ts +279 -0
  96. package/runner/src/adapters/codex.ts +1345 -0
  97. package/runner/src/attachment-cache.ts +189 -0
  98. package/runner/src/busy-reconciler.ts +102 -0
  99. package/runner/src/claim-tracker.ts +140 -0
  100. package/runner/src/claude-prompt-gates.ts +268 -0
  101. package/runner/src/config.ts +1 -4
  102. package/runner/src/context-probe-state.ts +6 -0
  103. package/runner/src/continuation-archive.ts +179 -0
  104. package/runner/src/control-server.ts +639 -0
  105. package/runner/src/index.ts +396 -0
  106. package/runner/src/launch-assembly.ts +508 -0
  107. package/runner/src/liveness.ts +50 -0
  108. package/runner/src/logger.ts +99 -0
  109. package/runner/src/mcp-outbox.ts +120 -0
  110. package/runner/src/message-body-config/config.ts +3 -0
  111. package/runner/src/message-body-config.ts +1 -0
  112. package/runner/src/native-self-resume.ts +202 -0
  113. package/runner/src/outbox.ts +342 -0
  114. package/runner/src/process-meta.ts +9 -0
  115. package/runner/src/profile-home.ts +260 -0
  116. package/runner/src/profile-projection.ts +305 -0
  117. package/runner/src/providers.ts +20 -0
  118. package/runner/src/provisioning.ts +314 -0
  119. package/runner/src/quota.ts +40 -0
  120. package/runner/src/rate-limit.ts +189 -0
  121. package/runner/src/relay-injection-events.ts +102 -0
  122. package/runner/src/relay-instructions.ts +60 -0
  123. package/runner/src/relay-mcp-proxy.ts +383 -0
  124. package/runner/src/relay-mcp.ts +156 -0
  125. package/runner/src/reply-obligation-cache.ts +136 -0
  126. package/runner/src/response-capture-report.ts +63 -0
  127. package/runner/src/runner-core.ts +2409 -0
  128. package/runner/src/runner-helpers.ts +435 -0
  129. package/runner/src/runner-insights.ts +105 -0
  130. package/runner/src/runner.ts +14 -0
  131. package/runner/src/session-destroy.ts +22 -0
  132. package/runner/src/session-insights.ts +118 -0
  133. package/runner/src/session-scratch.ts +271 -0
  134. package/runner/src/template-resolver.ts +29 -0
  135. package/runner/src/version.ts +43 -0
  136. package/src/agent-card-projection.ts +5 -1
  137. package/src/agent-command-activity.ts +39 -0
  138. package/src/automations/reconcile.ts +2 -2
  139. package/src/cli/workspace.ts +3 -1
  140. package/src/config.ts +10 -1
  141. package/src/db/agents.ts +118 -37
  142. package/src/db/message-reads.ts +50 -0
  143. package/src/db/messages.ts +48 -9
  144. package/src/db/migrations.ts +54 -0
  145. package/src/db/provider-quota-burn.ts +96 -67
  146. package/src/db/provider-quotas.ts +29 -7
  147. package/src/db/scheduled-timers.ts +21 -0
  148. package/src/db/schema.ts +1 -0
  149. package/src/db/tasks.ts +33 -3
  150. package/src/db/work-evidence.ts +10 -7
  151. package/src/event-loop-lag.ts +15 -0
  152. package/src/gate-resolver.ts +19 -1
  153. package/src/maintenance/jobs.ts +23 -6
  154. package/src/managed-policy.ts +5 -12
  155. package/src/mcp-plan-tools.ts +13 -1
  156. package/src/project-instructions.ts +36 -2
  157. package/src/quota-advisory.ts +6 -4
  158. package/src/quota-band-transition.ts +23 -14
  159. package/src/routes/agent-sessions.ts +39 -7
  160. package/src/routes/agents.ts +3 -2
  161. package/src/routes/tasks.ts +16 -4
  162. package/src/routes/workspaces.ts +35 -10
  163. package/src/services/plan-graph.ts +27 -9
  164. package/src/services/register-agent.ts +12 -12
  165. package/src/services/send-message.ts +43 -15
  166. package/src/services/spawn-agent.ts +3 -0
  167. package/src/spawn-command.ts +8 -0
  168. package/src/spawn-targets.ts +19 -34
  169. package/src/sse.ts +25 -3
  170. package/src/workspace-merge.ts +25 -5
  171. package/src/workspace-orphans.ts +13 -6
  172. package/public/assets/display-exQDWum3.js +0 -3
  173. package/public/assets/display-exQDWum3.js.map +0 -1
  174. package/public/assets/index-D9OogaB5.js +0 -25
  175. package/public/assets/projects-DdPNPFJI.js +0 -2
  176. package/public/assets/prompt-settings-BpZse7-X.js +0 -2
  177. package/public/assets/store-DKTLubBT.js.map +0 -1
  178. package/public/assets/teams-B8f2pGJe.js +0 -2
  179. package/public/assets/types-uVOXgvMT.js.map +0 -1
@@ -0,0 +1,435 @@
1
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
2
+ import type { AgentProfile, ContextState, Message, ProviderCapabilities, ProviderState, QuotaState } from "agent-relay-sdk";
3
+ import { errMessage, isSpawnProvider } from "agent-relay-sdk";
4
+ import { getManifest } from "agent-relay-providers";
5
+ import type { BooleanCapabilityDefault, ProviderCapabilityCondition } from "agent-relay-providers";
6
+ import { targetMatchesAgentIdentity } from "agent-relay-sdk/agent-target";
7
+ import type { ProviderAdapter, ProviderConfig, RunnerSpawnConfig, SemanticStatus } from "./adapter";
8
+ import { agentProfileProjectionReport } from "./profile-projection";
9
+ import { assembleLaunch } from "./launch-assembly";
10
+
11
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
12
+ const LOG_TAIL_BYTES = 128 * 1024;
13
+ const RUNNER_CLAIMED_GUIDANCE_RE = /\s*Claim this task before working it, then update task status when finished\.\s*$/;
14
+
15
+ interface RuntimeProviderOptions {
16
+ provider: string;
17
+ model?: string;
18
+ effort?: string;
19
+ approvalMode: string;
20
+ headless: boolean;
21
+ startedAt: number;
22
+ adapter: ProviderAdapter;
23
+ }
24
+
25
+ export interface ProbeModelInfo {
26
+ model?: string;
27
+ effort?: string;
28
+ }
29
+
30
+ export function runnerMessageMatches(message: Pick<Message, "to" | "resolvedToAgent">, target: {
31
+ agentId: string;
32
+ label?: string;
33
+ provider: string;
34
+ tags: string[];
35
+ capabilities: string[];
36
+ defaultTags: string[];
37
+ defaultCapabilities: string[];
38
+ allowBroadTargets?: boolean;
39
+ }): boolean {
40
+ if (message.resolvedToAgent === target.agentId) return true;
41
+ if (message.to === target.agentId) return true;
42
+ if (target.label && message.to === `label:${target.label}`) return true;
43
+ if (target.allowBroadTargets === false) return false;
44
+ return targetMatchesAgentIdentity(message.to, {
45
+ id: target.agentId,
46
+ label: target.label,
47
+ tags: [...target.tags, ...target.defaultTags, target.provider],
48
+ capabilities: [...target.capabilities, ...target.defaultCapabilities],
49
+ });
50
+ }
51
+
52
+ export function runnerMessageShouldDirtyObligations(message: Pick<Message, "from">, agentId: string): boolean {
53
+ return message.from !== agentId;
54
+ }
55
+
56
+ export function taskIdFromMessage(message: Pick<Message, "payload">): number | undefined {
57
+ const taskId = message.payload?.taskId;
58
+ return Number.isSafeInteger(taskId) ? taskId as number : undefined;
59
+ }
60
+
61
+ export function stripRunnerClaimedGuidance(body: string): string {
62
+ return body.replace(RUNNER_CLAIMED_GUIDANCE_RE, "");
63
+ }
64
+
65
+ // Bound a runner's INITIAL relay registration. `connect` resolves on the first `registered`
66
+ // frame and otherwise loops forever, so a token the relay rejects (401 at the WS upgrade)
67
+ // hangs here silently. Race it against `timeoutMs`; on timeout, surface a fatal reason, tear
68
+ // down the bus (stop the retry loop) and throw so the process exits and the orchestrator
69
+ // observes it instead of a silent zombie. Reconnects after first registration stay infinite.
70
+ export async function registerWithinDeadline(
71
+ bus: { connect: () => Promise<void>; close: () => Promise<void> },
72
+ timeoutMs: number,
73
+ relayUrl: string,
74
+ onFatal: (reason: string) => void,
75
+ ): Promise<void> {
76
+ let timer: ReturnType<typeof setTimeout> | undefined;
77
+ const deadline = new Promise<never>((_, reject) => {
78
+ timer = setTimeout(() => reject(new Error(
79
+ `runner failed to register with the relay within ${timeoutMs}ms — aborting so the orchestrator `
80
+ + `observes the exit instead of a silent hang (relay ${relayUrl}; check the relay is reachable `
81
+ + `and the runner token is valid).`,
82
+ )), timeoutMs);
83
+ });
84
+ try {
85
+ await Promise.race([bus.connect(), deadline]);
86
+ } catch (error) {
87
+ onFatal(errMessage(error));
88
+ await bus.close().catch(() => {});
89
+ throw error;
90
+ } finally {
91
+ if (timer) clearTimeout(timer);
92
+ }
93
+ }
94
+
95
+ export function csvTags(raw: string | undefined): string[] {
96
+ return (raw || "").split(",").map((tag) => tag.trim()).filter(Boolean);
97
+ }
98
+
99
+ export function relayBusUrl(relayUrl: string): string {
100
+ const url = new URL(relayUrl);
101
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
102
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/bus`;
103
+ url.search = "";
104
+ return url.toString();
105
+ }
106
+
107
+ export function runnerAgentStatus(status: "idle" | "busy" | "offline" | "error"): "idle" | "busy" | "offline" {
108
+ return status === "error" ? "offline" : status;
109
+ }
110
+
111
+ export function runnerBusErrorAction(code: string, stopped: boolean): "ignore" | "log" | "stop" {
112
+ if (code === "STALE_SESSION") return stopped ? "ignore" : "stop";
113
+ return "log";
114
+ }
115
+
116
+ export function runnerShouldResolveProviderExit(status: SemanticStatus, exitCommandInProgress: boolean): boolean {
117
+ return !exitCommandInProgress && (status === "offline" || status === "error");
118
+ }
119
+
120
+ export function runnerShouldRestartUnexpectedProviderExit(
121
+ status: SemanticStatus,
122
+ input: {
123
+ exitCommandInProgress: boolean;
124
+ stopped: boolean;
125
+ restartInProgress: boolean;
126
+ provider?: string;
127
+ headless: boolean;
128
+ hasTerminalSession: boolean;
129
+ },
130
+ ): boolean {
131
+ return status === "offline"
132
+ && !input.exitCommandInProgress
133
+ && !input.stopped
134
+ && !input.restartInProgress
135
+ && input.headless
136
+ && input.hasTerminalSession;
137
+ }
138
+
139
+ function resumeLogExtractionRe(provider: string): RegExp | undefined {
140
+ const manifest = getManifest(provider);
141
+ const resume = manifest?.resume as { supported: boolean; idPattern?: string } | undefined;
142
+ if (!resume?.supported || !resume.idPattern || !manifest?.probe?.command) return undefined;
143
+ return new RegExp(`\\b${manifest.probe.command}\\s+--resume\\s+(${resume.idPattern})\\b`, "gi");
144
+ }
145
+
146
+ export function latestResumeIdFromText(provider: string, text: string): string | undefined {
147
+ const re = resumeLogExtractionRe(provider);
148
+ if (!re) return undefined;
149
+ let latest: string | undefined;
150
+ for (let match = re.exec(text); match; match = re.exec(text)) {
151
+ latest = match[1];
152
+ }
153
+ return latest;
154
+ }
155
+
156
+ export function latestResumeIdFromLogFile(provider: string, path: string): string | undefined {
157
+ let fd: number | undefined;
158
+ try {
159
+ const stat = statSync(path);
160
+ const length = Math.min(stat.size, LOG_TAIL_BYTES);
161
+ const offset = Math.max(0, stat.size - length);
162
+ const buffer = Buffer.alloc(length);
163
+ fd = openSync(path, "r");
164
+ readSync(fd, buffer, 0, length, offset);
165
+ return latestResumeIdFromText(provider, buffer.toString("utf8"));
166
+ } catch {
167
+ return undefined;
168
+ } finally {
169
+ if (fd !== undefined) closeSync(fd);
170
+ }
171
+ }
172
+
173
+ export function providerExitDiagnostics(input: {
174
+ status: SemanticStatus;
175
+ runtimeMs: number;
176
+ processMeta?: Record<string, unknown>;
177
+ hasProcess: boolean;
178
+ exitCommandInProgress: boolean;
179
+ stopped: boolean;
180
+ restartInProgress: boolean;
181
+ restartPending: boolean;
182
+ headless: boolean;
183
+ provider: string;
184
+ logFile?: string;
185
+ }): Record<string, unknown> {
186
+ const tmuxSession = typeof input.processMeta?.tmuxSession === "string" ? input.processMeta.tmuxSession : undefined;
187
+ const tmuxSocket = typeof input.processMeta?.tmuxSocket === "string" ? input.processMeta.tmuxSocket : undefined;
188
+ const exitSource = tmuxSession ? "tmux-session-ended" : input.hasProcess ? "process-exit" : "provider-status";
189
+ const claudeResumeId = getManifest(input.provider)?.resume?.supported && input.logFile ? latestResumeIdFromLogFile(input.provider, input.logFile) : undefined;
190
+ return {
191
+ status: input.status,
192
+ runtimeMs: Number.isFinite(input.runtimeMs) ? input.runtimeMs : null,
193
+ exitSource,
194
+ exitCommandInProgress: input.exitCommandInProgress,
195
+ stopped: input.stopped,
196
+ restartInProgress: input.restartInProgress,
197
+ restartPending: input.restartPending,
198
+ headless: input.headless,
199
+ hasTerminalSession: Boolean(tmuxSession),
200
+ tmuxSession: tmuxSession ?? null,
201
+ tmuxSocket: tmuxSocket ?? null,
202
+ claudeResumeId: claudeResumeId ?? null,
203
+ };
204
+ }
205
+
206
+ export function commandTimeoutMs(params: Record<string, unknown>, fallback = 10_000): number {
207
+ const raw = params.timeoutMs;
208
+ if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw <= 0) return fallback;
209
+ return Math.min(raw, 60_000);
210
+ }
211
+
212
+ export function shouldLogDeliveryFailure(error: unknown): boolean {
213
+ const message = errMessage(error);
214
+ return message !== "no Claude monitor connected";
215
+ }
216
+
217
+ export function runtimeTokenRenewDelayMs(expiresAtSeconds: number, nowMs = Date.now()): number | undefined {
218
+ const expiresAtMs = expiresAtSeconds * 1000;
219
+ const ttlMs = expiresAtMs - nowMs;
220
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) return undefined;
221
+ const leadMs = Math.min(60 * 60 * 1000, Math.max(60_000, Math.floor(ttlMs * 0.2)));
222
+ const jitterMs = Math.floor(Math.random() * Math.min(30_000, Math.max(0, leadMs / 2)));
223
+ return Math.max(1_000, ttlMs - leadMs - jitterMs);
224
+ }
225
+
226
+ export function runtimeTokenRenewTimerSchedule(delayMs: number): { delayMs: number; renew: boolean } | undefined {
227
+ if (!Number.isFinite(delayMs) || delayMs <= 0) return undefined;
228
+ if (delayMs > MAX_TIMER_DELAY_MS) return { delayMs: MAX_TIMER_DELAY_MS, renew: false };
229
+ return { delayMs, renew: true };
230
+ }
231
+
232
+ export function lifecycleCapabilities(): Record<string, true> {
233
+ return {
234
+ shutdownHard: true,
235
+ restartHard: true,
236
+ semanticStatus: true,
237
+ transportReconnect: true,
238
+ };
239
+ }
240
+
241
+ function resolveCondition(condition: ProviderCapabilityCondition, options: RuntimeProviderOptions): boolean {
242
+ switch (condition) {
243
+ case "always": return true;
244
+ case "never": return false;
245
+ case "headless": return options.headless;
246
+ case "interactive": return !options.headless;
247
+ case "approval:read-only": return options.approvalMode === "read-only";
248
+ case "approval:not-read-only": return options.approvalMode !== "read-only";
249
+ case "adapter:compact-supports-instructions": return Boolean(options.adapter.compactSupportsInstructions);
250
+ case "adapter:clear-context": return Boolean(options.adapter.clearContext);
251
+ default: return false;
252
+ }
253
+ }
254
+
255
+ function resolveBooleanDefault(def: BooleanCapabilityDefault, options: RuntimeProviderOptions): boolean {
256
+ if (typeof def === "boolean") return def;
257
+ return resolveCondition(def.when, options) ? def.value : def.otherwise;
258
+ }
259
+
260
+ export function runtimeProviderCapabilities(options: RuntimeProviderOptions, contextState?: ContextState, probeModel?: ProbeModelInfo, quotaState?: QuotaState): ProviderCapabilities {
261
+ const manifest = getManifest(options.provider);
262
+ const capDefaults = manifest?.capabilityDefaults;
263
+ const model = options.model ?? probeModel?.model;
264
+ const effort = options.effort ?? probeModel?.effort;
265
+ const modelSource = options.model ? "runtime" as const : probeModel?.model ? "provider" as const : "runtime" as const;
266
+ const shellMode = runtimeShellMode(options);
267
+ return {
268
+ lifecycle: {
269
+ managed: capDefaults?.lifecycle?.managed ?? true,
270
+ shutdownHard: capDefaults?.lifecycle?.shutdownHard !== undefined ? resolveBooleanDefault(capDefaults.lifecycle.shutdownHard, options) : true,
271
+ restartHard: capDefaults?.lifecycle?.restartHard !== undefined ? resolveBooleanDefault(capDefaults.lifecycle.restartHard, options) : true,
272
+ semanticStatus: capDefaults?.lifecycle?.semanticStatus ?? true,
273
+ reconnect: capDefaults?.lifecycle?.reconnect ?? true,
274
+ },
275
+ model: {
276
+ provider: options.provider,
277
+ alias: model,
278
+ id: model,
279
+ effort,
280
+ source: modelSource,
281
+ confidence: options.model ? "reported" : probeModel?.model ? "reported" : "unknown",
282
+ lastUpdatedAt: options.startedAt,
283
+ },
284
+ session: {
285
+ approvalMode: options.approvalMode,
286
+ fileRead: true,
287
+ fileWrite: options.approvalMode !== "read-only",
288
+ shell: shellMode !== "none",
289
+ shellMode,
290
+ source: "runtime",
291
+ confidence: "reported",
292
+ lastUpdatedAt: options.startedAt,
293
+ },
294
+ ...runtimeProviderContextCapabilities(options, contextState),
295
+ ...runtimeProviderQuotaCapabilities(options, quotaState),
296
+ ...runtimeProviderTerminalCapabilities(options),
297
+ liveSession: {
298
+ capture: true,
299
+ inject: Boolean(options.adapter.deliverInitialPrompt),
300
+ interrupt: Boolean(options.adapter.interrupt),
301
+ promptEcho: true,
302
+ reasoning: true,
303
+ slashCommands: capDefaults?.liveSession?.slashCommands ?? false,
304
+ },
305
+ source: "runtime",
306
+ confidence: "reported",
307
+ lastUpdatedAt: options.startedAt,
308
+ };
309
+ }
310
+
311
+ function runtimeShellMode(options: RuntimeProviderOptions): NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]> {
312
+ const manifest = getManifest(options.provider);
313
+ const shellModeMap = manifest?.capabilityDefaults?.session?.shellMode;
314
+ if (options.approvalMode === "read-only") return (shellModeMap?.readOnly ?? "none") as NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]>;
315
+ if (options.approvalMode === "open") return (shellModeMap?.open ?? "unrestricted") as NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]>;
316
+ return (shellModeMap?.guarded ?? "guarded") as NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]>;
317
+ }
318
+
319
+ function runtimeProviderTerminalCapabilities(options: RuntimeProviderOptions): Pick<ProviderCapabilities, "terminal"> {
320
+ const manifest = getManifest(options.provider);
321
+ const termDefaults = manifest?.capabilityDefaults?.terminal;
322
+ if (!termDefaults || termDefaults.mode === "none") return {};
323
+ if (termDefaults.condition && !resolveCondition(termDefaults.condition, options)) return {};
324
+ if (termDefaults.mode === "live" && termDefaults.live) {
325
+ return { terminal: { live: termDefaults.live } };
326
+ }
327
+ if (termDefaults.mode === "attach" && termDefaults.attach) {
328
+ return { terminal: { attach: termDefaults.attach } };
329
+ }
330
+ return {};
331
+ }
332
+
333
+ // #557 — the agent-card projection now DESCRIBES the assembled launch (same source the
334
+ // adapter turns into args), so it can't drift from reality. `config`/`providerConfig` are
335
+ // the spawn inputs; when present we assemble (pure — no disk writes here) and report that.
336
+ export function appliedAgentProfileMetadata(
337
+ provider: string,
338
+ profile: AgentProfile,
339
+ config?: RunnerSpawnConfig,
340
+ providerConfig?: ProviderConfig,
341
+ ): Record<string, unknown> {
342
+ const projection = isSpawnProvider(provider) && config && providerConfig
343
+ ? agentProfileProjectionReport({ assembled: assembleLaunch(provider, config, providerConfig) })
344
+ : undefined;
345
+ return {
346
+ name: profile.name,
347
+ base: profile.base,
348
+ provider: profile.provider ?? "any",
349
+ relay: profile.relay,
350
+ instructions: {
351
+ repoInstructions: profile.instructions.repoInstructions,
352
+ globalInstructions: profile.instructions.globalInstructions,
353
+ appendCount: profile.instructions.append.length,
354
+ hasSystem: Boolean(profile.instructions.system),
355
+ },
356
+ skills: profile.skills.filter((item) => item.enabled).length,
357
+ plugins: profile.plugins.filter((item) => item.enabled).length,
358
+ mcp: { mode: profile.mcp.mode },
359
+ hooks: { mode: profile.hooks.mode },
360
+ permissions: profile.permissions,
361
+ projection,
362
+ };
363
+ }
364
+
365
+ function runtimeProviderContextCapabilities(options: RuntimeProviderOptions, contextState?: ContextState): Pick<ProviderCapabilities, "context"> {
366
+ const manifest = getManifest(options.provider);
367
+ const ctxDefaults = manifest?.capabilityDefaults?.context;
368
+ const context: NonNullable<ProviderCapabilities["context"]> = { resume: "none" };
369
+ const supportsManagedContext = ctxDefaults?.managed ? resolveCondition(ctxDefaults.managed, options) : false;
370
+ if (contextState) {
371
+ context.stats = { source: contextState.source, confidence: contextState.confidence };
372
+ if (typeof contextState.tokensMax === "number" && Number.isFinite(contextState.tokensMax)) {
373
+ context.windowTokens = contextState.tokensMax;
374
+ }
375
+ }
376
+ if (ctxDefaults) {
377
+ if (resolveBooleanDefault(ctxDefaults.compact, options)) context.compact = true;
378
+ if (resolveBooleanDefault(ctxDefaults.clear, options)) context.clear = true;
379
+ context.inject = ctxDefaults.inject;
380
+ }
381
+ if (supportsManagedContext && options.adapter.compact && options.adapter.compactSupportsInstructions) context.resume = "native";
382
+ else if (supportsManagedContext && options.adapter.clearContext) context.resume = "clear-inject";
383
+ return Object.keys(context).length ? { context } : {};
384
+ }
385
+
386
+ function runtimeProviderQuotaCapabilities(options: RuntimeProviderOptions, quotaState?: QuotaState): Pick<ProviderCapabilities, "quota"> {
387
+ const manifest = getManifest(options.provider);
388
+ const quotaDefaults = manifest?.capabilityDefaults?.quota;
389
+ if (!quotaDefaults?.supported) return {};
390
+ return {
391
+ quota: {
392
+ supported: true,
393
+ source: quotaState?.source ?? "probe",
394
+ unit: quotaState?.windows[0]?.unit ?? "%",
395
+ windows: quotaState?.windows.map((window) => ({
396
+ name: window.name,
397
+ unit: window.unit,
398
+ reset: window.resetsAt !== undefined,
399
+ })) ?? manifest!.quotaWindows.map(w => ({ name: w.name, unit: w.unit as "%" | "tokens" | "requests", reset: w.reset })),
400
+ },
401
+ };
402
+ }
403
+
404
+ export function providerStateFromActiveWork(activeWork: Array<{ kind: string; metadata?: Record<string, unknown> }>): ProviderState | null {
405
+ const providerTurn = activeWork.find((item) => item.kind === "provider-turn");
406
+ const state = providerTurn?.metadata?.providerState;
407
+ return state && typeof state === "object" && !Array.isArray(state) && typeof (state as { state?: unknown }).state === "string"
408
+ ? state as ProviderState
409
+ : null;
410
+ }
411
+
412
+ export function isHttpAuthError(error: unknown): boolean {
413
+ const status = typeof error === "object" && error !== null ? (error as { status?: unknown }).status : undefined;
414
+ return status === 401 || status === 403;
415
+ }
416
+
417
+ export function isHttpStatusError(error: unknown, code: number): boolean {
418
+ const status = typeof error === "object" && error !== null ? (error as { status?: unknown }).status : undefined;
419
+ return status === code;
420
+ }
421
+
422
+ export function httpErrorKey(error: unknown): string {
423
+ const status = typeof error === "object" && error !== null ? (error as { status?: unknown }).status : undefined;
424
+ if (typeof status === "number") return `status:${status}`;
425
+ return String(error);
426
+ }
427
+
428
+ export function isContextState(value: unknown): value is ContextState {
429
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
430
+ const state = value as Record<string, unknown>;
431
+ return typeof state.utilization === "number" &&
432
+ typeof state.lifecycleState === "string" &&
433
+ typeof state.source === "string" &&
434
+ typeof state.confidence === "string";
435
+ }
@@ -0,0 +1,105 @@
1
+ import type { WorkspaceMetadata } from "agent-relay-sdk";
2
+ import type { ManagedProcess, ProviderAdapter } from "./adapter";
3
+ import type { Outbox } from "./outbox";
4
+ import type { SessionDestroyReason } from "./session-destroy";
5
+ import { resolveProjectName } from "./config";
6
+ import { computeContextRatio } from "./session-insights";
7
+ import { boundContinuationArchiveSegment } from "./continuation-archive";
8
+
9
+ interface RunnerInsightsDeps {
10
+ agentId: string;
11
+ cwd: string;
12
+ workspace?: WorkspaceMetadata;
13
+ getOutbox(): Outbox;
14
+ getProviderSessionId(): string;
15
+ getProcess(): ManagedProcess | undefined;
16
+ getAdapter(): ProviderAdapter;
17
+ getLastTranscriptPath(): string | undefined;
18
+ sessionLog(message: string): void;
19
+ }
20
+
21
+ export class RunnerInsights {
22
+ private insightsObserved = 0;
23
+ private insightsCursorKey = "";
24
+ private archiveObservedChars = 0;
25
+ private archiveCursorKey = "";
26
+ private projectName?: string;
27
+
28
+ constructor(private readonly deps: RunnerInsightsDeps) {}
29
+
30
+ project(): string {
31
+ return (this.projectName ??= resolveProjectName(this.deps.cwd, this.deps.workspace));
32
+ }
33
+
34
+ queueHookFatal(report: { hook: string; error: string }): void {
35
+ this.deps.getOutbox().enqueue({
36
+ kind: "insight",
37
+ payload: {
38
+ sessionId: this.deps.getProviderSessionId(),
39
+ project: this.project(),
40
+ agentId: this.deps.agentId,
41
+ signal: "hook_fatal",
42
+ value: { hook: report.hook, error: report.error },
43
+ source: "server",
44
+ },
45
+ });
46
+ }
47
+
48
+ async captureContinuationArchive(reason: SessionDestroyReason, opts?: { transcriptPath?: string }): Promise<void> {
49
+ const adapter = this.deps.getAdapter();
50
+ const process = this.deps.getProcess();
51
+ if (!adapter.collectSessionArchiveSegment || !process) return;
52
+ const transcriptPath = opts?.transcriptPath ?? this.deps.getLastTranscriptPath();
53
+ const archive = await adapter.collectSessionArchiveSegment(process, { transcriptPath });
54
+ if (!archive) return;
55
+ const key = transcriptPath ?? `session:${this.deps.getProviderSessionId()}`;
56
+ if (key !== this.archiveCursorKey || archive.length < this.archiveObservedChars) {
57
+ this.archiveCursorKey = key;
58
+ this.archiveObservedChars = 0;
59
+ }
60
+ const segment = archive.slice(this.archiveObservedChars).trim();
61
+ this.archiveObservedChars = archive.length;
62
+ if (!segment) return;
63
+ const bounded = boundContinuationArchiveSegment(segment);
64
+ if (bounded.droppedBytes > 0) this.deps.sessionLog(`continuation archive truncated at ${bounded.keptBytes} bytes; dropped ${bounded.droppedBytes} bytes (${reason})`);
65
+ this.deps.getOutbox().enqueue({
66
+ kind: "continuation-archive",
67
+ payload: {
68
+ agentId: this.deps.agentId,
69
+ segment: bounded.segment,
70
+ },
71
+ });
72
+ this.deps.sessionLog(`continuation archive queued (${bounded.segment.length} chars, ${reason})`);
73
+ }
74
+
75
+ async captureContextRatio(reason: SessionDestroyReason, opts?: { transcriptPath?: string }): Promise<void> {
76
+ const adapter = this.deps.getAdapter();
77
+ const process = this.deps.getProcess();
78
+ if (!adapter.collectSessionEvents || !process) return;
79
+ const transcriptPath = opts?.transcriptPath ?? this.deps.getLastTranscriptPath();
80
+ const events = await adapter.collectSessionEvents(process, { transcriptPath });
81
+ if (!events) return;
82
+ const key = transcriptPath ?? `session:${this.deps.getProviderSessionId()}`;
83
+ if (key !== this.insightsCursorKey || events.length < this.insightsObserved) {
84
+ this.insightsCursorKey = key;
85
+ this.insightsObserved = 0;
86
+ }
87
+ const segment = events.slice(this.insightsObserved);
88
+ this.insightsObserved = events.length;
89
+ const analysis = computeContextRatio(segment);
90
+ if (!analysis) return;
91
+ this.deps.getOutbox().enqueue({
92
+ kind: "insight",
93
+ payload: {
94
+ sessionId: this.deps.getProviderSessionId(),
95
+ project: this.project(),
96
+ agentId: this.deps.agentId,
97
+ signal: "context_ratio",
98
+ value: { ...analysis.metric, endReason: reason },
99
+ outcome: { ...analysis.outcome },
100
+ source: "server",
101
+ },
102
+ });
103
+ this.deps.sessionLog(`insights: context_ratio ${analysis.metric.ratio.toFixed(2)} (${analysis.metric.gatheringCalls}/${analysis.metric.totalToolCalls} gathering, ${reason}) queued`);
104
+ }
105
+ }
@@ -0,0 +1,14 @@
1
+ export { AgentRunner } from "./runner-core";
2
+ export {
3
+ latestResumeIdFromText,
4
+ runnerAgentStatus,
5
+ runnerBusErrorAction,
6
+ runnerMessageMatches,
7
+ runnerShouldResolveProviderExit,
8
+ runnerShouldRestartUnexpectedProviderExit,
9
+ runtimeTokenRenewDelayMs,
10
+ runtimeTokenRenewTimerSchedule,
11
+ shouldLogDeliveryFailure,
12
+ stripRunnerClaimedGuidance,
13
+ taskIdFromMessage,
14
+ } from "./runner-helpers";
@@ -0,0 +1,22 @@
1
+ export type SessionDestroyReason = "compact" | "clear" | "restart" | "shutdown" | "kill" | "crash";
2
+
3
+ export type LifecycleAction =
4
+ | "shutting-down" | "killing" | "restarting"
5
+ | `finalizing-${SessionDestroyReason}`;
6
+
7
+ export const PRE_DESTROY_TIMEOUT_MS = 4_000;
8
+
9
+ export function reasonExitsRunner(reason: SessionDestroyReason): boolean {
10
+ return reason === "shutdown" || reason === "kill" || reason === "crash";
11
+ }
12
+
13
+ export function boundaryReasonForCommand(type: string): SessionDestroyReason | undefined {
14
+ switch (type) {
15
+ case "agent.compact": return "compact";
16
+ case "agent.clearContext": return "clear";
17
+ case "agent.restart": return "restart";
18
+ case "agent.shutdown": return "shutdown";
19
+ case "agent.kill": return "kill";
20
+ default: return undefined;
21
+ }
22
+ }