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,314 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
4
+ import type { ProvisioningAssetFile, ProvisioningMcpServer, ResolvedProvisioning } from "agent-relay-sdk";
5
+ import type { RunnerSpawnConfig } from "./adapter";
6
+ import { profileAllowsHostAssets, providerSourceHome } from "./profile-home";
7
+
8
+ // Runner-side materialization of the relay-resolved provisioning bundle
9
+ // (#554/#555/#556). The relay resolves a profile's refs against the DB registry and
10
+ // attaches the concrete definitions to profile.provisioning; here we turn those into
11
+ // real on-disk dirs (skills/plugins). MCP is handled in relay-mcp.ts.
12
+ //
13
+ // #557 — split into PURE RESOLUTION (resolve* → where each asset lands, no writes) and
14
+ // MATERIALIZATION (materialize* → the disk writes). The single launch assembler
15
+ // (launch-assembly.ts) calls the resolvers to build BOTH the launch args and the
16
+ // projection report, so what's reported can never drift from what's written; the
17
+ // materialize* writers consume the same resolved entries, so what's written can never
18
+ // drift from what's reported either. One resolution, three consumers.
19
+
20
+ const EMPTY: ResolvedProvisioning = { skills: [], plugins: [], mcpServers: {} };
21
+
22
+ export function profileProvisioning(config: Pick<RunnerSpawnConfig, "agentProfile">): ResolvedProvisioning {
23
+ const p = config.agentProfile?.provisioning;
24
+ if (!p) return EMPTY;
25
+ return { skills: p.skills ?? [], plugins: p.plugins ?? [], mcpServers: p.mcpServers ?? {}, ...(p.skipped?.length ? { skipped: p.skipped } : {}) };
26
+ }
27
+
28
+ // The `mcpServers` keys a Claude `.mcp.json` entry may carry; coerced into the
29
+ // provider-neutral ProvisioningMcpServer shape so the launch assembler projects them the
30
+ // same way it does relay-provisioned servers (Claude `--mcp-config`, Codex `-c mcp_servers.*`).
31
+ function coerceProjectMcpServer(raw: unknown): ProvisioningMcpServer | null {
32
+ if (!raw || typeof raw !== "object") return null;
33
+ const r = raw as Record<string, unknown>;
34
+ const server: ProvisioningMcpServer = {};
35
+ if (r.type === "stdio" || r.type === "http" || r.type === "sse") server.type = r.type;
36
+ if (typeof r.command === "string") server.command = r.command;
37
+ if (Array.isArray(r.args) && r.args.every((a) => typeof a === "string")) server.args = r.args as string[];
38
+ if (r.env && typeof r.env === "object") server.env = r.env as Record<string, string>;
39
+ if (r.configEnv && typeof r.configEnv === "object") server.configEnv = r.configEnv as Record<string, string>;
40
+ if (typeof r.url === "string") server.url = r.url;
41
+ if (r.headers && typeof r.headers === "object") server.headers = r.headers as Record<string, string>;
42
+ // Drop an entry that names no transport (neither a command nor a url) — nothing to launch.
43
+ return server.command || server.url ? server : null;
44
+ }
45
+
46
+ // #667 — project `.mcp.json` MCP projection. OPT-IN via the resolved profile's `projectMcp`
47
+ // (set per-spawn or by profile, relay-side). Reads the spawn cwd's COMMITTED `.mcp.json` and
48
+ // returns its `mcpServers` as a provider-neutral record, so the launch assembler injects them
49
+ // through the CONTROLLED `--mcp-config` (Claude) / `-c mcp_servers.*` (Codex) path that
50
+ // survives `--strict-mcp-config` — the project's own servers travel with the repo like they do
51
+ // for a human who clones it, WITHOUT enabling Claude's ambient project-discovery trust modal
52
+ // (which wedges headless agents, the reason #662 made every managed launch strict). The runner
53
+ // is the filesystem authority for the spawn cwd (relay may run on another host), so the read
54
+ // lands here. Best-effort: a missing/malformed file (or one with no `mcpServers`) yields {} and
55
+ // never aborts the launch. Returns {} when projection is OFF.
56
+ export function resolveProjectMcpServers(
57
+ config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd">,
58
+ ): Record<string, ProvisioningMcpServer> {
59
+ if (!config.agentProfile?.projectMcp) return {};
60
+ const file = join(config.cwd, ".mcp.json");
61
+ if (!existsSync(file)) return {};
62
+ let parsed: unknown;
63
+ try {
64
+ parsed = JSON.parse(readFileSync(file, "utf8"));
65
+ } catch (err) {
66
+ console.warn(`project .mcp.json projection: failed to parse ${file}: ${(err as Error).message}`);
67
+ return {};
68
+ }
69
+ const servers = (parsed as { mcpServers?: unknown } | null)?.mcpServers;
70
+ if (!servers || typeof servers !== "object") return {};
71
+ const out: Record<string, ProvisioningMcpServer> = {};
72
+ for (const [name, raw] of Object.entries(servers as Record<string, unknown>)) {
73
+ const server = coerceProjectMcpServer(raw);
74
+ if (server) out[name] = server;
75
+ }
76
+ return out;
77
+ }
78
+
79
+ export function resolveClaudeProjectSkills(
80
+ claudeHome: string,
81
+ config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd">,
82
+ reservedNames: ReadonlySet<string> = new Set(),
83
+ ): ResolvedAsset[] {
84
+ if (!config.agentProfile?.projectSkills) return [];
85
+ const skillsRoot = join(config.cwd, ".claude", "skills");
86
+ if (!existsSync(skillsRoot)) return [];
87
+ const targetRoot = join(claudeHome, "skills");
88
+ const resolved: ResolvedAsset[] = [];
89
+ for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
90
+ if (!entry.isDirectory() || reservedNames.has(entry.name)) continue;
91
+ const source = join(skillsRoot, entry.name);
92
+ if (!existsSync(join(source, "SKILL.md"))) continue;
93
+ resolved.push({
94
+ name: entry.name,
95
+ dir: join(targetRoot, assetDirName(entry.name)),
96
+ source: { kind: "link", source },
97
+ });
98
+ }
99
+ return resolved.sort((a, b) => a.name.localeCompare(b.name));
100
+ }
101
+
102
+ export function resolveClaudeHostSkills(
103
+ claudeHome: string,
104
+ config: Pick<RunnerSpawnConfig, "agentProfile">,
105
+ reservedNames: ReadonlySet<string> = new Set(),
106
+ ): ResolvedAsset[] {
107
+ if (!profileAllowsHostAssets(config, "skills")) return [];
108
+ const skillsRoot = join(providerSourceHome("claude"), "skills");
109
+ if (!existsSync(skillsRoot)) return [];
110
+ const targetRoot = join(claudeHome, "skills");
111
+ const resolved: ResolvedAsset[] = [];
112
+ for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
113
+ if (!entry.isDirectory() || reservedNames.has(entry.name)) continue;
114
+ const source = join(skillsRoot, entry.name);
115
+ if (!existsSync(join(source, "SKILL.md"))) continue;
116
+ resolved.push({
117
+ name: entry.name,
118
+ dir: join(targetRoot, assetDirName(entry.name)),
119
+ source: { kind: "link", source },
120
+ });
121
+ }
122
+ return resolved.sort((a, b) => a.name.localeCompare(b.name));
123
+ }
124
+
125
+ const UNSAFE_PROJECT_CLAUDE_SETTINGS = new Set([
126
+ "apiKeyHelper",
127
+ "allowDangerouslySkipPermissions",
128
+ "allowedTools",
129
+ "disabledMcpjsonServers",
130
+ "disallowedTools",
131
+ "enableAllProjectMcpServers",
132
+ "enabledMcpjsonServers",
133
+ "env",
134
+ "hooks",
135
+ "mcpServers",
136
+ "permissionMode",
137
+ "permissions",
138
+ "statusLine",
139
+ "tools",
140
+ ]);
141
+
142
+ export interface ResolvedProjectClaudeSettings {
143
+ applied: boolean;
144
+ settings: Record<string, unknown>;
145
+ keys: string[];
146
+ ignoredKeys: string[];
147
+ file?: string;
148
+ }
149
+
150
+ export function resolveProjectClaudeSettings(
151
+ config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd">,
152
+ ): ResolvedProjectClaudeSettings {
153
+ if (!config.agentProfile?.projectSettings) return { applied: false, settings: {}, keys: [], ignoredKeys: [] };
154
+ const file = join(config.cwd, ".claude", "settings.json");
155
+ if (!existsSync(file)) return { applied: true, settings: {}, keys: [], ignoredKeys: [], file };
156
+ let parsed: unknown;
157
+ try {
158
+ parsed = JSON.parse(readFileSync(file, "utf8"));
159
+ } catch (err) {
160
+ console.warn(`project .claude/settings.json projection: failed to parse ${file}: ${(err as Error).message}`);
161
+ return { applied: true, settings: {}, keys: [], ignoredKeys: [], file };
162
+ }
163
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
164
+ return { applied: true, settings: {}, keys: [], ignoredKeys: [], file };
165
+ }
166
+ const settings: Record<string, unknown> = {};
167
+ const ignoredKeys: string[] = [];
168
+ for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
169
+ if (UNSAFE_PROJECT_CLAUDE_SETTINGS.has(key)) {
170
+ ignoredKeys.push(key);
171
+ continue;
172
+ }
173
+ settings[key] = value;
174
+ }
175
+ return {
176
+ applied: true,
177
+ settings,
178
+ keys: Object.keys(settings).sort(),
179
+ ignoredKeys: ignoredKeys.sort(),
180
+ file,
181
+ };
182
+ }
183
+
184
+ // A resolved provisioning asset: WHERE it lands (`dir`) and HOW it materializes
185
+ // (`source`). `files` writes the inline set into `dir`; `link` symlinks a pre-existing
186
+ // source dir at `dir`; `inPlace` uses the source dir as-is (no write, dir IS the source).
187
+ export type ResolvedAssetSource =
188
+ | { kind: "files"; files: ProvisioningAssetFile[] }
189
+ | { kind: "link"; source: string }
190
+ | { kind: "inPlace" };
191
+ export interface ResolvedAsset {
192
+ name: string;
193
+ dir: string;
194
+ source: ResolvedAssetSource;
195
+ }
196
+
197
+ function assetDirName(name: string): string {
198
+ return sanitizeFsName(name, { replacement: "-", collapse: false, maxLen: 120, fallback: "asset" });
199
+ }
200
+
201
+ function resolveExistingDir(dir: string, cwd: string): string | null {
202
+ const abs = isAbsolute(dir) ? dir : resolve(cwd, dir);
203
+ return existsSync(abs) ? abs : null;
204
+ }
205
+
206
+ function hasRootSkillManifest(files: ProvisioningAssetFile[]): boolean {
207
+ return files.some((f) => f.path === "SKILL.md");
208
+ }
209
+
210
+ // Write an inline file set into <dir>, guarding against path escapes from a malformed
211
+ // registry entry. Shared by every files-source materialization.
212
+ function writeInlineFiles(dir: string, files: ProvisioningAssetFile[]): void {
213
+ mkdirSync(dir, { recursive: true });
214
+ for (const file of files) {
215
+ const dest = resolve(dir, file.path);
216
+ if (!dest.startsWith(resolve(dir))) continue;
217
+ mkdirSync(dirname(dest), { recursive: true });
218
+ writeFileSync(dest, file.content, { mode: 0o600 });
219
+ }
220
+ }
221
+
222
+ // Perform the disk write a resolved asset describes. files → write inline; link →
223
+ // (re)create the symlink; inPlace → nothing (the dir already exists at the source).
224
+ // Best-effort for links: a failed link must not abort the launch.
225
+ function materializeAsset(asset: ResolvedAsset): boolean {
226
+ if (asset.source.kind === "files") {
227
+ writeInlineFiles(asset.dir, asset.source.files);
228
+ return true;
229
+ }
230
+ if (asset.source.kind === "link") {
231
+ try {
232
+ mkdirSync(dirname(asset.dir), { recursive: true });
233
+ if (existsSync(asset.dir)) rmSync(asset.dir, { recursive: true, force: true });
234
+ symlinkSync(asset.source.source, asset.dir);
235
+ return true;
236
+ } catch {
237
+ return false;
238
+ }
239
+ }
240
+ return existsSync(asset.dir);
241
+ }
242
+
243
+ // #554 (Claude) — each provisioned skill lands at <claudeHome>/skills/<name> so it's
244
+ // auto-discovered under the launch's `--setting-sources "user"` scope (host skills
245
+ // never bleed). Inline files are written there; a pre-existing dir is symlinked in.
246
+ export function resolveClaudeProvisionedSkills(claudeHome: string, config: RunnerSpawnConfig): ResolvedAsset[] {
247
+ const { skills } = profileProvisioning(config);
248
+ const skillsRoot = join(claudeHome, "skills");
249
+ const resolved: ResolvedAsset[] = [];
250
+ for (const skill of skills) {
251
+ const dir = join(skillsRoot, assetDirName(skill.name));
252
+ if (skill.files?.length) {
253
+ resolved.push({ name: skill.name, dir, source: { kind: "files", files: skill.files } });
254
+ continue;
255
+ }
256
+ if (skill.dir) {
257
+ const source = resolveExistingDir(skill.dir, config.cwd);
258
+ if (source) resolved.push({ name: skill.name, dir, source: { kind: "link", source } });
259
+ }
260
+ }
261
+ return resolved;
262
+ }
263
+
264
+ // #556 (Claude) — each provisioned plugin resolves to a --plugin-dir. Inline plugins
265
+ // materialize under <claudeHome>/relay-provisioned-plugins/<name>; declared dirs are
266
+ // used in place. The resolved `dir` is appended to the launch's --plugin-dir set.
267
+ export function resolveClaudeProvisionedPlugins(claudeHome: string, config: RunnerSpawnConfig): ResolvedAsset[] {
268
+ const { plugins } = profileProvisioning(config);
269
+ const parent = join(claudeHome, "relay-provisioned-plugins");
270
+ const resolved: ResolvedAsset[] = [];
271
+ for (const plugin of plugins) {
272
+ if (plugin.files?.length) {
273
+ resolved.push({ name: plugin.name, dir: join(parent, assetDirName(plugin.name)), source: { kind: "files", files: plugin.files } });
274
+ continue;
275
+ }
276
+ if (plugin.dir) {
277
+ const source = resolveExistingDir(plugin.dir, config.cwd);
278
+ if (source) resolved.push({ name: plugin.name, dir: source, source: { kind: "inPlace" } });
279
+ }
280
+ }
281
+ return resolved;
282
+ }
283
+
284
+ // #554 (Codex) — each provisioned skill resolves to a dir for `-c skills.config`.
285
+ // Inline skills materialize under <codexHome>/relay-provisioned-skills/<name>; declared
286
+ // dirs are used in place. Codex requires a SKILL.md at the dir root to register it, so
287
+ // inline sets without one and dirs missing it are dropped at resolution time.
288
+ export function resolveCodexProvisionedSkills(codexHome: string, config: RunnerSpawnConfig): ResolvedAsset[] {
289
+ const { skills } = profileProvisioning(config);
290
+ const parent = join(codexHome, "relay-provisioned-skills");
291
+ const resolved: ResolvedAsset[] = [];
292
+ for (const skill of skills) {
293
+ if (skill.files?.length) {
294
+ if (!hasRootSkillManifest(skill.files)) continue;
295
+ resolved.push({ name: skill.name, dir: join(parent, assetDirName(skill.name)), source: { kind: "files", files: skill.files } });
296
+ continue;
297
+ }
298
+ if (skill.dir) {
299
+ const source = resolveExistingDir(skill.dir, config.cwd);
300
+ if (source && existsSync(join(source, "SKILL.md"))) resolved.push({ name: skill.name, dir: source, source: { kind: "inPlace" } });
301
+ }
302
+ }
303
+ return resolved;
304
+ }
305
+
306
+ // Write the resolved assets to disk; returns the dirs that materialized successfully,
307
+ // in resolution order. Idempotent — safe to re-run against an existing home.
308
+ export function materializeResolvedAssets(assets: ResolvedAsset[]): string[] {
309
+ const dirs: string[] = [];
310
+ for (const asset of assets) {
311
+ if (materializeAsset(asset)) dirs.push(asset.dir);
312
+ }
313
+ return dirs;
314
+ }
@@ -0,0 +1,40 @@
1
+ export {
2
+ CLAUDE_OAUTH_BETA,
3
+ CLAUDE_API_VERSION,
4
+ CLAUDE_MESSAGES_URL,
5
+ CLAUDE_QUOTA_PROBE_MODEL,
6
+ CLAUDE_USAGE_URL,
7
+ DEFAULT_PROVIDER_QUOTA_CONFIG,
8
+ QUOTA_FAILURE_LOG_INTERVAL_MS,
9
+ QUOTA_FAST_RETRY_MS,
10
+ QUOTA_POLL_INTERVAL_MS,
11
+ QuotaCollectionError,
12
+ QuotaRetryAfterError,
13
+ claudeRateLimitHeadersQuotaState,
14
+ claudeUsageQuotaState,
15
+ collectClaudeQuotaSample,
16
+ collectClaudeQuotaState,
17
+ collectCodexQuotaSample,
18
+ collectCodexQuotaState,
19
+ codexRateLimitsQuotaState,
20
+ codexWhamResetCredits,
21
+ codexWhamUsageQuotaState,
22
+ credentialAccountKey,
23
+ defaultClaudeConfigDir,
24
+ defaultClaudeCredentialsPath,
25
+ hostProviderAccountKey,
26
+ normalizeProviderQuotaConfig,
27
+ providerPayloadAccountKey,
28
+ providerQuotaErrorFromCollectorError,
29
+ quotaRetryAfterMs,
30
+ readClaudeOAuthAccessToken,
31
+ resolveClaudeQuotaIdentity,
32
+ resolveClaudeStatuslineQuotaIdentity,
33
+ resolveCodexQuotaIdentity,
34
+ resolveCodexQuotaIdentityFromHome,
35
+ resolveStableClaudeQuotaIdentity,
36
+ resolveStableCodexQuotaIdentity,
37
+ resolveStableCodexQuotaIdentityFromHome,
38
+ type ProviderQuotaIdentity,
39
+ type ProviderQuotaSample,
40
+ } from "agent-relay-sdk/provider-quota";
@@ -0,0 +1,189 @@
1
+ // #286 — shared rate-limit hold construction + Claude pane detection.
2
+ //
3
+ // Two detection arms feed the SAME provider-neutral `blocked` hold:
4
+ // 1. The StopFailure hook (clean API-error turn-end: API-429/auth/billing) → control-server.
5
+ // 2. Pane-scrape of the subscription "session/weekly limit" modal (no hook exists for it —
6
+ // anthropics/claude-code#34817 was closed not-planned) → the Claude adapter.
7
+ // Both build the hold here so the shape stays identical, and the relay's resume sweep
8
+ // (services/rate-limit-resume.ts) lifts either once the window resets.
9
+
10
+ import type { ProviderState } from "agent-relay-sdk";
11
+
12
+ export const RATE_LIMIT_BLOCK_REASON = "rate_limit";
13
+
14
+ // Reject a parsed reset that's in the past or implausibly far out — a bad parse would
15
+ // otherwise resume too early (thrash) or hold for days. Beyond this we drop resetAt and
16
+ // fall back to the resume sweep's poll window.
17
+ const MAX_RESET_AHEAD_MS = 7 * 24 * 60 * 60 * 1000;
18
+
19
+ interface RateLimitHoldInput {
20
+ errorType?: string;
21
+ kind?: "usage_limit" | "transient_overload";
22
+ /** Unix ms the limit window resets, when known. */
23
+ resetAt?: number;
24
+ message?: string;
25
+ source?: string;
26
+ }
27
+
28
+ /** The blocked providerState a rate-limit hold carries. Shared by both detection arms. */
29
+ export function buildRateLimitProviderState(input: RateLimitHoldInput): ProviderState {
30
+ const now = Date.now();
31
+ const resetAt = typeof input.resetAt === "number" && Number.isFinite(input.resetAt) ? input.resetAt : undefined;
32
+ const kind = input.kind ?? rateLimitKindForError(input.errorType);
33
+ const label = kind === "transient_overload"
34
+ ? "provider overloaded"
35
+ : resetAt ? `usage limit · resets ${formatResetClock(resetAt)}` : "usage limit reached";
36
+ return {
37
+ state: "blocked",
38
+ reason: RATE_LIMIT_BLOCK_REASON,
39
+ label,
40
+ recommendedAction: kind === "transient_overload"
41
+ ? "Holding briefly for provider overload; agent-relay will retry with bounded backoff."
42
+ : "Holding until the usage window resets; agent-relay auto-resumes the agent.",
43
+ kind,
44
+ source: input.source ?? "claude",
45
+ errorType: input.errorType ?? RATE_LIMIT_BLOCK_REASON,
46
+ enteredAt: now,
47
+ ...(resetAt ? { resetAt } : {}),
48
+ ...(input.message ? { message: input.message } : {}),
49
+ updatedAt: now,
50
+ };
51
+ }
52
+
53
+ function rateLimitKindForError(errorType: string | undefined): "usage_limit" | "transient_overload" {
54
+ return /^(overloaded|server_error|api_error|unknown|529|5\d\d)$/i.test(errorType ?? "") ? "transient_overload" : "usage_limit";
55
+ }
56
+
57
+ /** Host-local HH:MM for the chat notice / badge label (matches how the CLI shows "resets 3:45pm"). */
58
+ function formatResetClock(resetAtMs: number): string {
59
+ try {
60
+ return new Date(resetAtMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
61
+ } catch {
62
+ return new Date(resetAtMs).toISOString().slice(11, 16);
63
+ }
64
+ }
65
+
66
+ // The subscription limit line wordings seen in the wild (claude-auto-retry patterns +
67
+ // claude-auto-retry#15: "session"/"weekly limit" slipped past older regexes). We REQUIRE a
68
+ // limit phrase AND a reset/retry phrase on the same line so normal output mentioning
69
+ // "rate limit" can't false-positive the hold.
70
+ const LIMIT_PHRASE_RE = /(?:hit (?:your|the)\s*(?:[\w-]+\s+)*limit|usage limit|\d+-hour limit|limit reached|out of (?:extra )?usage|rate limit(?:\s+(?:hit|reached|exceeded))?|weekly limit|session limit)/i;
71
+ const RESET_AT_RE = /\bresets?\b(?:\s+at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*(?:\(([^)]+)\))?/i;
72
+ const TRY_AGAIN_RE = /\btry again in\s+(\d+)\s*(second|minute|hour|day)s?/i;
73
+
74
+ interface PaneRateLimit {
75
+ /** The matched limit line, for the chat notice / observability. */
76
+ message: string;
77
+ /** Unix ms reset, when parseable from the pane (else undefined → resume falls back to poll). */
78
+ resetAt?: number;
79
+ /** Usage limits wait for reset; transient provider overloads retry quickly. */
80
+ kind?: "usage_limit" | "transient_overload";
81
+ errorType?: string;
82
+ }
83
+
84
+ /**
85
+ * Detect a Claude usage/rate-limit modal in a captured tmux pane and parse its reset time.
86
+ * Requires BOTH a limit phrase and a reset/retry phrase (anywhere in the pane, so it's robust
87
+ * to a modal that wraps the two onto separate lines) — that pairing is what distinguishes the
88
+ * real modal from normal output mentioning "rate limit". Returns null otherwise. `nowMs` is
89
+ * injectable for tests.
90
+ */
91
+ export function parseClaudeRateLimitPane(text: string, nowMs: number = Date.now()): PaneRateLimit | null {
92
+ if (!text) return null;
93
+ const overload = parseClaudeTransientOverloadPane(text);
94
+ if (overload) return overload;
95
+ const limit = LIMIT_PHRASE_RE.exec(text);
96
+ if (!limit) return null;
97
+ const message = limitLineAt(text, limit.index);
98
+
99
+ const relative = TRY_AGAIN_RE.exec(text);
100
+ if (relative) {
101
+ const unitMs = { second: 1000, minute: 60_000, hour: 3_600_000, day: 86_400_000 }[relative[2]!.toLowerCase()] ?? 0;
102
+ const resetAt = clampReset(nowMs + Number(relative[1]) * unitMs, nowMs);
103
+ return { message, ...(resetAt ? { resetAt } : {}) };
104
+ }
105
+ const reset = RESET_AT_RE.exec(text);
106
+ if (reset) {
107
+ const resetAt = clampReset(parseClockReset(reset, nowMs), nowMs);
108
+ return { message, ...(resetAt ? { resetAt } : {}) };
109
+ }
110
+ // Limit phrase but no reset/retry anywhere — not confident it's the modal; skip to
111
+ // avoid false-positiving on normal output (docs, the agent's own text) that says "limit".
112
+ return null;
113
+ }
114
+
115
+ const TRANSIENT_API_ERROR_RE = /\bAPI Error:\s*((?:5\d\d)|529)\b[^\n]*(?:overload|server-side|temporar|try again|status\.claude\.com)?[^\n]*/i;
116
+ const TRANSIENT_5XX_RE = /\b(?:5(?:00|02|03|04|20|29)|529)\b[^\n]*(?:overload|server-side|temporar|try again|status\.claude\.com)\b[^\n]*/i;
117
+
118
+ function parseClaudeTransientOverloadPane(text: string): PaneRateLimit | null {
119
+ const match = TRANSIENT_API_ERROR_RE.exec(text) ?? TRANSIENT_5XX_RE.exec(text);
120
+ if (!match) return null;
121
+ const message = limitLineAt(text, match.index);
122
+ const code = match[1] ?? /\b(5\d\d|529)\b/.exec(match[0])?.[1];
123
+ return {
124
+ message: message || match[0].trim(),
125
+ kind: "transient_overload",
126
+ errorType: code === "529" ? "overloaded" : "server_error",
127
+ };
128
+ }
129
+
130
+ // The clean limit line containing the match offset, stripped of box-drawing chrome.
131
+ function limitLineAt(text: string, index: number): string {
132
+ const start = text.lastIndexOf("\n", index) + 1;
133
+ const endNl = text.indexOf("\n", index);
134
+ const end = endNl === -1 ? text.length : endNl;
135
+ return text.slice(start, end).replace(/[│┌┐└┘─┤├╭╮╯╰]/g, " ").replace(/\s+/g, " ").trim();
136
+ }
137
+
138
+ function clampReset(resetAt: number | undefined, nowMs: number): number | undefined {
139
+ if (resetAt === undefined || !Number.isFinite(resetAt)) return undefined;
140
+ if (resetAt <= nowMs || resetAt - nowMs > MAX_RESET_AHEAD_MS) return undefined;
141
+ return resetAt;
142
+ }
143
+
144
+ // Next occurrence of a wall-clock "H[:MM] am/pm" in the message's IANA timezone (if given
145
+ // and valid) or host-local. Timezone-aware so "resets 4:50pm (Asia/Shanghai)" is correct
146
+ // regardless of where the runner host sits.
147
+ function parseClockReset(match: RegExpExecArray, nowMs: number): number | undefined {
148
+ let hour = Number(match[1]);
149
+ const minute = match[2] ? Number(match[2]) : 0;
150
+ const meridiem = match[3]?.toLowerCase();
151
+ const tz = match[4]?.trim();
152
+ if (!Number.isFinite(hour) || hour > 23 || minute > 59) return undefined;
153
+ if (meridiem === "pm" && hour < 12) hour += 12;
154
+ if (meridiem === "am" && hour === 12) hour = 0;
155
+
156
+ if (tz && isValidTimeZone(tz)) {
157
+ const { y, mo, d, offsetMs } = tzWallAndOffset(tz, nowMs);
158
+ let target = Date.UTC(y, mo - 1, d, hour, minute, 0) - offsetMs;
159
+ if (target <= nowMs) target += 86_400_000;
160
+ return target;
161
+ }
162
+ const d = new Date(nowMs);
163
+ d.setHours(hour, minute, 0, 0);
164
+ let target = d.getTime();
165
+ if (target <= nowMs) target += 86_400_000;
166
+ return target;
167
+ }
168
+
169
+ function isValidTimeZone(tz: string): boolean {
170
+ try {
171
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
172
+ return true;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ // The target TZ's wall-clock date and UTC offset at `atMs`, via Intl (no deps).
179
+ function tzWallAndOffset(tz: string, atMs: number): { y: number; mo: number; d: number; offsetMs: number } {
180
+ const dtf = new Intl.DateTimeFormat("en-US", {
181
+ timeZone: tz, hour12: false,
182
+ year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
183
+ });
184
+ const parts = Object.fromEntries(dtf.formatToParts(new Date(atMs)).map((p) => [p.type, p.value])) as Record<string, string>;
185
+ const y = Number(parts.year), mo = Number(parts.month), d = Number(parts.day);
186
+ const h = Number(parts.hour) % 24, mi = Number(parts.minute), s = Number(parts.second);
187
+ const asUtc = Date.UTC(y, mo - 1, d, h, mi, s);
188
+ return { y, mo, d, offsetMs: asUtc - atMs };
189
+ }
@@ -0,0 +1,102 @@
1
+ import type { MessageSessionMeta, RelayInjectionCategory } from "agent-relay-sdk";
2
+ import { isSpawnProvider } from "agent-relay-sdk";
3
+ import type { ProviderConfig, RunnerSpawnConfig } from "./adapter";
4
+ import { assembleLaunch } from "./launch-assembly";
5
+
6
+ export interface RunnerRelayInjectionEvent {
7
+ category: RelayInjectionCategory;
8
+ summary: string;
9
+ detail: Record<string, unknown>;
10
+ }
11
+
12
+ interface RunnerRelayInjectionSessionEvent {
13
+ from: string;
14
+ to: string;
15
+ body: string;
16
+ session: MessageSessionMeta;
17
+ }
18
+
19
+ interface KeyedRelayInjectionEvent {
20
+ key: string;
21
+ event: RunnerRelayInjectionEvent;
22
+ }
23
+
24
+ interface RunnerLaunchInjectionInput {
25
+ carried?: RunnerRelayInjectionEvent[];
26
+ provider: string;
27
+ config: RunnerSpawnConfig;
28
+ providerConfig: ProviderConfig;
29
+ agentId: string;
30
+ providerSessionId: string;
31
+ timestamp?: number;
32
+ }
33
+
34
+ function launchInjectionEvents(input: RunnerLaunchInjectionInput): KeyedRelayInjectionEvent[] {
35
+ const events: KeyedRelayInjectionEvent[] = (input.carried ?? []).map((event, index) => ({ key: `spawn-${index}`, event }));
36
+ if (!isSpawnProvider(input.provider)) return events;
37
+ const assembled = assembleLaunch(input.provider, input.config, input.providerConfig);
38
+ const toolCount = assembled.skills.length + assembled.plugins.length + assembled.mcp.servers.length
39
+ + assembled.projectSkills.length
40
+ + assembled.mcp.projectServers.length
41
+ + (assembled.projectSettings.applied ? 1 : 0)
42
+ + (assembled.relaySkills ? 1 : 0)
43
+ + (assembled.relayPlugin ? 1 : 0)
44
+ + (assembled.mcp.relayEndpoint ? 1 : 0)
45
+ + (assembled.statusLine ? 1 : 0);
46
+ if (toolCount === 0) return events;
47
+ const parts = [
48
+ assembled.relaySkills ? "relay skills" : "",
49
+ assembled.relayPlugin ? "relay plugin" : "",
50
+ assembled.statusLine ? "status line" : "",
51
+ assembled.mcp.relayEndpoint ? "relay MCP" : "",
52
+ assembled.skills.length ? `${assembled.skills.length} skill${assembled.skills.length === 1 ? "" : "s"}` : "",
53
+ assembled.projectSkills.length ? `${assembled.projectSkills.length} project skill${assembled.projectSkills.length === 1 ? "" : "s"}` : "",
54
+ assembled.projectSettings.applied ? "project settings" : "",
55
+ assembled.plugins.length ? `${assembled.plugins.length} plugin${assembled.plugins.length === 1 ? "" : "s"}` : "",
56
+ assembled.mcp.servers.length ? `${assembled.mcp.servers.length} MCP server${assembled.mcp.servers.length === 1 ? "" : "s"}` : "",
57
+ assembled.mcp.projectServers.length ? `${assembled.mcp.projectServers.length} project MCP server${assembled.mcp.projectServers.length === 1 ? "" : "s"}` : "",
58
+ ].filter(Boolean);
59
+ events.push({
60
+ key: "tools",
61
+ event: {
62
+ category: "tools",
63
+ summary: `provisioned ${parts.join(", ")}`,
64
+ detail: {
65
+ source: "launch-assembly",
66
+ provider: assembled.provider,
67
+ base: assembled.base,
68
+ vanilla: assembled.vanilla,
69
+ relayContext: assembled.relayContext,
70
+ relaySkills: assembled.relaySkills,
71
+ relayPlugin: assembled.relayPlugin,
72
+ statusLine: assembled.statusLine,
73
+ skills: assembled.skills,
74
+ projectSkills: assembled.projectSkills,
75
+ projectSettings: assembled.projectSettings,
76
+ plugins: assembled.plugins,
77
+ mcp: assembled.mcp,
78
+ },
79
+ },
80
+ });
81
+ return events;
82
+ }
83
+
84
+ export function runnerLaunchInjectionSessionEvents(input: RunnerLaunchInjectionInput): RunnerRelayInjectionSessionEvent[] {
85
+ const timestamp = input.timestamp ?? Date.now();
86
+ return launchInjectionEvents(input).map(({ event, key }) => ({
87
+ from: input.agentId,
88
+ to: "user",
89
+ body: event.summary,
90
+ session: {
91
+ type: "relay-injection",
92
+ stepId: `relay-injection:${input.providerSessionId}:${key}`,
93
+ relayInjection: {
94
+ category: event.category,
95
+ summary: event.summary,
96
+ detail: event.detail,
97
+ agentId: input.agentId,
98
+ timestamp,
99
+ },
100
+ },
101
+ }));
102
+ }