|
| 1 | +// Agent identity parser — teammate/subagent names from the wire header. |
| 2 | +// |
| 3 | +// Claude Code 2.x sends an `x-claude-code-agent-id` request header on every |
| 4 | +// sub-agent (teammate or subagent) request. Two shapes appear in real data: |
| 5 | +// - named teammate: `frontend-reviewer@session-17e1f37a-...` |
| 6 | +// - anonymous subagent: `a7eea0a140349f80d` (16-hex agent id, no @) |
| 7 | +// The header is absent on main-agent requests, so its presence alone marks a |
| 8 | +// sub-agent stream. We persist the NAME so display no longer depends on the |
| 9 | +// frontend's window-scoped heuristic registry (resolveTeammateNames), which |
| 10 | +// loses names when the lead's Agent tool_use falls outside the current window. |
| 11 | + |
| 12 | +const ANON_RE = /^[0-9a-f]{8,64}$/i; // pure hex id, any plausible length |
| 13 | +const MAX_NAME_LEN = 64; |
| 14 | + |
| 15 | +/** |
| 16 | + * Parse an `x-claude-code-agent-id` header value. |
| 17 | + * |
| 18 | + * @param {string|null|undefined} header |
| 19 | + * @returns {{agentName: string|null, named: boolean}|null} |
| 20 | + * - named teammate: {agentName, named: true} |
| 21 | + * - anonymous agent: {agentName: null, named: false} |
| 22 | + * - null when the header is missing/empty or its shape is not an agent id |
| 23 | + * (never fabricate a name from an unrecognized value). |
| 24 | + */ |
| 25 | +export function parseAgentId(header) { |
| 26 | + if (typeof header !== 'string') return null; |
| 27 | + const h = header.trim(); |
| 28 | + if (h.length === 0) return null; |
| 29 | + const at = h.indexOf('@'); |
| 30 | + if (at === -1) { |
| 31 | + // Anonymous: a bare hex agent id. Anything else is not an agent id — do |
| 32 | + // not guess. |
| 33 | + return ANON_RE.test(h) ? { agentName: null, named: false } : null; |
| 34 | + } |
| 35 | + const name = h.slice(0, at).trim(); |
| 36 | + // The @-suffix is deliberately NOT validated (real shapes include |
| 37 | + // `session-<uuid>` and possibly plain uuids) — presence of @ + a non-hex |
| 38 | + // prefix is enough to treat it as a named agent. A hex prefix is treated as |
| 39 | + // anonymous (guards against `<uuid>@session-...` being misread as a name). |
| 40 | + if (name.length === 0) return null; |
| 41 | + if (ANON_RE.test(name)) return { agentName: null, named: false }; |
| 42 | + return { agentName: name.slice(0, MAX_NAME_LEN), named: true }; |
| 43 | +} |
| 44 | + |
| 45 | +/** Case-insensitive header lookup (undici folds to lowercase; plain objects |
| 46 | + * may keep the original casing). */ |
| 47 | +export function findHeader(headers, key) { |
| 48 | + if (!headers || typeof headers !== 'object') return undefined; |
| 49 | + if (Object.prototype.hasOwnProperty.call(headers, key)) return headers[key]; |
| 50 | + const lower = key.toLowerCase(); |
| 51 | + for (const k of Object.keys(headers)) { |
| 52 | + if (k.toLowerCase() === lower) return headers[k]; |
| 53 | + } |
| 54 | + return undefined; |
| 55 | +} |
0 commit comments