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.
- package/docs/openapi.json +1 -1
- package/package.json +6 -6
- package/public/assets/{MessageBubble-CLqxCG9b.js → MessageBubble-DRdUD-kW.js} +2 -2
- package/public/assets/{MessageBubble-CLqxCG9b.js.map → MessageBubble-DRdUD-kW.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BkkToFEb.js → PlanGraphPanel-siKMPCln.js} +2 -2
- package/public/assets/{PlanGraphPanel-BkkToFEb.js.map → PlanGraphPanel-siKMPCln.js.map} +1 -1
- package/public/assets/{activity-D-ocGQGs.js → activity-D3MNcSaV.js} +2 -2
- package/public/assets/{activity-D-ocGQGs.js.map → activity-D3MNcSaV.js.map} +1 -1
- package/public/assets/{agent-profiles-CRPgYTyb.js → agent-profiles-CKzttq6G.js} +2 -2
- package/public/assets/{agent-profiles-CRPgYTyb.js.map → agent-profiles-CKzttq6G.js.map} +1 -1
- package/public/assets/{agent-type-icon-CeEoMouY.js → agent-type-icon-0ZJRP1Vy.js} +2 -2
- package/public/assets/{agent-type-icon-CeEoMouY.js.map → agent-type-icon-0ZJRP1Vy.js.map} +1 -1
- package/public/assets/{agents-Chw5E_2q.js → agents-CuVj_rfG.js} +2 -2
- package/public/assets/{agents-Chw5E_2q.js.map → agents-CuVj_rfG.js.map} +1 -1
- package/public/assets/{analytics-BmNDG0hR.js → analytics-iob6C27d.js} +2 -2
- package/public/assets/{analytics-BmNDG0hR.js.map → analytics-iob6C27d.js.map} +1 -1
- package/public/assets/{automation-D7g90kTv.js → automation-B2ixxs4q.js} +2 -2
- package/public/assets/{automation-D7g90kTv.js.map → automation-B2ixxs4q.js.map} +1 -1
- package/public/assets/{branch-state-badge-Bv7DhLBZ.js → branch-state-badge-CafBJWKo.js} +2 -2
- package/public/assets/{branch-state-badge-Bv7DhLBZ.js.map → branch-state-badge-CafBJWKo.js.map} +1 -1
- package/public/assets/{channels-CLwPeEPi.js → channels-C-WsbL2H.js} +2 -2
- package/public/assets/{channels-CLwPeEPi.js.map → channels-C-WsbL2H.js.map} +1 -1
- package/public/assets/{chat-Cgj7VKzc.js → chat-DeoVTVob.js} +2 -2
- package/public/assets/{chat-Cgj7VKzc.js.map → chat-DeoVTVob.js.map} +1 -1
- package/public/assets/{connectors-Br6fiTny.js → connectors-C2Ac03LJ.js} +2 -2
- package/public/assets/{connectors-Br6fiTny.js.map → connectors-C2Ac03LJ.js.map} +1 -1
- package/public/assets/display-CJzSoTMq.js +3 -0
- package/public/assets/display-CJzSoTMq.js.map +1 -0
- package/public/assets/{formatted-body-CmkuD23M.js → formatted-body-35XBOY5L.js} +3 -3
- package/public/assets/{formatted-body-CmkuD23M.js.map → formatted-body-35XBOY5L.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DbdFmbwW.js → formatted-body-impl-RmDxI5Ux.js} +2 -2
- package/public/assets/{formatted-body-impl-DbdFmbwW.js.map → formatted-body-impl-RmDxI5Ux.js.map} +1 -1
- package/public/assets/index-D0xMQQb_.js +25 -0
- package/public/assets/{index-D9OogaB5.js.map → index-D0xMQQb_.js.map} +1 -1
- package/public/assets/{insights-qvaPqWCV.js → insights-B643SFvm.js} +2 -2
- package/public/assets/{insights-qvaPqWCV.js.map → insights-B643SFvm.js.map} +1 -1
- package/public/assets/{integrations-DYEGSqq_.js → integrations-hFIHeCF4.js} +2 -2
- package/public/assets/{integrations-DYEGSqq_.js.map → integrations-hFIHeCF4.js.map} +1 -1
- package/public/assets/{maintenance-C0HIEB-J.js → maintenance-CURvdp_Z.js} +2 -2
- package/public/assets/{maintenance-C0HIEB-J.js.map → maintenance-CURvdp_Z.js.map} +1 -1
- package/public/assets/{managed-agents-DdS7aI38.js → managed-agents-DrZtdlSn.js} +2 -2
- package/public/assets/{managed-agents-DdS7aI38.js.map → managed-agents-DrZtdlSn.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BnXMH1z1.js → markdown-preview-impl-CVDO--ek.js} +2 -2
- package/public/assets/{markdown-preview-impl-BnXMH1z1.js.map → markdown-preview-impl-CVDO--ek.js.map} +1 -1
- package/public/assets/{memory-CSwx7bz8.js → memory-Bm7dt_Qf.js} +2 -2
- package/public/assets/{memory-CSwx7bz8.js.map → memory-Bm7dt_Qf.js.map} +1 -1
- package/public/assets/{messages-Be9CmvDv.js → messages-DTq4i2KZ.js} +2 -2
- package/public/assets/{messages-Be9CmvDv.js.map → messages-DTq4i2KZ.js.map} +1 -1
- package/public/assets/{orchestrators-BgtvvHvo.js → orchestrators-Dra318Ap.js} +2 -2
- package/public/assets/{orchestrators-BgtvvHvo.js.map → orchestrators-Dra318Ap.js.map} +1 -1
- package/public/assets/{overview-BvhbPWtF.js → overview-yr1Vmk64.js} +2 -2
- package/public/assets/{overview-BvhbPWtF.js.map → overview-yr1Vmk64.js.map} +1 -1
- package/public/assets/{pairs-CpXSOMx0.js → pairs-mnlKDBGp.js} +2 -2
- package/public/assets/{pairs-CpXSOMx0.js.map → pairs-mnlKDBGp.js.map} +1 -1
- package/public/assets/projects-Dzdf-gkK.js +2 -0
- package/public/assets/{projects-DdPNPFJI.js.map → projects-Dzdf-gkK.js.map} +1 -1
- package/public/assets/prompt-settings-ByL8SNCi.js +2 -0
- package/public/assets/{prompt-settings-BpZse7-X.js.map → prompt-settings-ByL8SNCi.js.map} +1 -1
- package/public/assets/{registry-pb1gTZEg.js → registry-CXWyOtpr.js} +2 -2
- package/public/assets/{registry-pb1gTZEg.js.map → registry-CXWyOtpr.js.map} +1 -1
- package/public/assets/{security-BgcX1n9D.js → security-0SBsc_4W.js} +2 -2
- package/public/assets/{security-BgcX1n9D.js.map → security-0SBsc_4W.js.map} +1 -1
- package/public/assets/{select-DZPVpKPv.js → select-Bw5z2KJ_.js} +2 -2
- package/public/assets/{select-DZPVpKPv.js.map → select-Bw5z2KJ_.js.map} +1 -1
- package/public/assets/{settings-hd5kzdRB.js → settings-78Cyq_tA.js} +4 -4
- package/public/assets/{settings-hd5kzdRB.js.map → settings-78Cyq_tA.js.map} +1 -1
- package/public/assets/{store-DKTLubBT.js → store-BPrVuKiv.js} +4 -4
- package/public/assets/store-BPrVuKiv.js.map +1 -0
- package/public/assets/{tasks-C5d2LU5E.js → tasks-Cg2MMeri.js} +2 -2
- package/public/assets/{tasks-C5d2LU5E.js.map → tasks-Cg2MMeri.js.map} +1 -1
- package/public/assets/teams-DWv6UwFV.js +2 -0
- package/public/assets/{teams-B8f2pGJe.js.map → teams-DWv6UwFV.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CxAscH0U.js → terminal-viewer-impl-DBLDjbIg.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CxAscH0U.js.map → terminal-viewer-impl-DBLDjbIg.js.map} +1 -1
- package/public/assets/{types-uVOXgvMT.js → types-BeJUSfDj.js} +2 -2
- package/public/assets/types-BeJUSfDj.js.map +1 -0
- package/public/assets/{user-avatar-Dw6mzWhv.js → user-avatar-BsOLyVJj.js} +2 -2
- package/public/assets/{user-avatar-Dw6mzWhv.js.map → user-avatar-BsOLyVJj.js.map} +1 -1
- package/public/assets/{work-queue-9imc7aNK.js → work-queue-DmwvnF6F.js} +2 -2
- package/public/assets/{work-queue-9imc7aNK.js.map → work-queue-DmwvnF6F.js.map} +1 -1
- package/public/assets/{workspaces-CLYc3yF3.js → workspaces-dIBivJN8.js} +2 -2
- package/public/assets/{workspaces-CLYc3yF3.js.map → workspaces-dIBivJN8.js.map} +1 -1
- package/public/index.html +6 -6
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +44 -938
- package/runner/src/adapter.ts +53 -18
- package/runner/src/adapters/claude-delivery.ts +127 -0
- package/runner/src/adapters/claude-quota-harvest.ts +92 -0
- package/runner/src/adapters/claude-session-probe.ts +225 -0
- package/runner/src/adapters/claude-status-detectors.ts +147 -0
- package/runner/src/adapters/claude-transcript-tail.ts +128 -0
- package/runner/src/adapters/claude-transcript.ts +414 -0
- package/runner/src/adapters/claude.ts +672 -0
- package/runner/src/adapters/codex-agent-message-capture.ts +139 -0
- package/runner/src/adapters/codex-client.ts +279 -0
- package/runner/src/adapters/codex.ts +1345 -0
- package/runner/src/attachment-cache.ts +189 -0
- package/runner/src/busy-reconciler.ts +102 -0
- package/runner/src/claim-tracker.ts +140 -0
- package/runner/src/claude-prompt-gates.ts +268 -0
- package/runner/src/config.ts +1 -4
- package/runner/src/context-probe-state.ts +6 -0
- package/runner/src/continuation-archive.ts +179 -0
- package/runner/src/control-server.ts +639 -0
- package/runner/src/index.ts +396 -0
- package/runner/src/launch-assembly.ts +508 -0
- package/runner/src/liveness.ts +50 -0
- package/runner/src/logger.ts +99 -0
- package/runner/src/mcp-outbox.ts +120 -0
- package/runner/src/message-body-config/config.ts +3 -0
- package/runner/src/message-body-config.ts +1 -0
- package/runner/src/native-self-resume.ts +202 -0
- package/runner/src/outbox.ts +342 -0
- package/runner/src/process-meta.ts +9 -0
- package/runner/src/profile-home.ts +260 -0
- package/runner/src/profile-projection.ts +305 -0
- package/runner/src/providers.ts +20 -0
- package/runner/src/provisioning.ts +314 -0
- package/runner/src/quota.ts +40 -0
- package/runner/src/rate-limit.ts +189 -0
- package/runner/src/relay-injection-events.ts +102 -0
- package/runner/src/relay-instructions.ts +60 -0
- package/runner/src/relay-mcp-proxy.ts +383 -0
- package/runner/src/relay-mcp.ts +156 -0
- package/runner/src/reply-obligation-cache.ts +136 -0
- package/runner/src/response-capture-report.ts +63 -0
- package/runner/src/runner-core.ts +2409 -0
- package/runner/src/runner-helpers.ts +435 -0
- package/runner/src/runner-insights.ts +105 -0
- package/runner/src/runner.ts +14 -0
- package/runner/src/session-destroy.ts +22 -0
- package/runner/src/session-insights.ts +118 -0
- package/runner/src/session-scratch.ts +271 -0
- package/runner/src/template-resolver.ts +29 -0
- package/runner/src/version.ts +43 -0
- package/src/agent-card-projection.ts +5 -1
- package/src/agent-command-activity.ts +39 -0
- package/src/automations/reconcile.ts +2 -2
- package/src/cli/workspace.ts +3 -1
- package/src/config.ts +10 -1
- package/src/db/agents.ts +118 -37
- package/src/db/message-reads.ts +50 -0
- package/src/db/messages.ts +48 -9
- package/src/db/migrations.ts +54 -0
- package/src/db/provider-quota-burn.ts +96 -67
- package/src/db/provider-quotas.ts +29 -7
- package/src/db/scheduled-timers.ts +21 -0
- package/src/db/schema.ts +1 -0
- package/src/db/tasks.ts +33 -3
- package/src/db/work-evidence.ts +10 -7
- package/src/event-loop-lag.ts +15 -0
- package/src/gate-resolver.ts +19 -1
- package/src/maintenance/jobs.ts +23 -6
- package/src/managed-policy.ts +5 -12
- package/src/mcp-plan-tools.ts +13 -1
- package/src/project-instructions.ts +36 -2
- package/src/quota-advisory.ts +6 -4
- package/src/quota-band-transition.ts +23 -14
- package/src/routes/agent-sessions.ts +39 -7
- package/src/routes/agents.ts +3 -2
- package/src/routes/tasks.ts +16 -4
- package/src/routes/workspaces.ts +35 -10
- package/src/services/plan-graph.ts +27 -9
- package/src/services/register-agent.ts +12 -12
- package/src/services/send-message.ts +43 -15
- package/src/services/spawn-agent.ts +3 -0
- package/src/spawn-command.ts +8 -0
- package/src/spawn-targets.ts +19 -34
- package/src/sse.ts +25 -3
- package/src/workspace-merge.ts +25 -5
- package/src/workspace-orphans.ts +13 -6
- package/public/assets/display-exQDWum3.js +0 -3
- package/public/assets/display-exQDWum3.js.map +0 -1
- package/public/assets/index-D9OogaB5.js +0 -25
- package/public/assets/projects-DdPNPFJI.js +0 -2
- package/public/assets/prompt-settings-BpZse7-X.js +0 -2
- package/public/assets/store-DKTLubBT.js.map +0 -1
- package/public/assets/teams-B8f2pGJe.js +0 -2
- 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
|
+
}
|