Skip to content

Inject current user device context into agent runtime#33

Open
artile wants to merge 2 commits intojakemor:mainfrom
tao-io:pr/current-user-device-context
Open

Inject current user device context into agent runtime#33
artile wants to merge 2 commits intojakemor:mainfrom
tao-io:pr/current-user-device-context

Conversation

@artile
Copy link
Copy Markdown
Contributor

@artile artile commented Mar 28, 2026

Summary

  • detect the current user device in the browser and send it to the server as client context
  • inject CURRENT_USER_DEVICE and CURRENT_USER_DEVICE_LABEL into agent runtime environments
  • pass matching runtime instructions into Claude and Codex turns so simple device questions are answered directly

Notes

Verification

  • rtk bash -lc '/root/.bun/bin/bun run check'

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 28, 2026

Greptile Summary

This PR propagates browser-detected device context from the client through the WebSocket layer into Claude and Codex agent runtimes, allowing the AI to answer device-related questions without spawning shell commands.

Key changes:

  • src/client/lib/current-user-device.ts — new UA-sniffing module that maps navigator signals to a CurrentUserDevice tag (pixel, android, ios, macbook, windows, linux, unknown), with Pixel detected before generic Android
  • src/client/app/socket.ts — sends system.setClientContext immediately on every WebSocket open, ensuring the server re-receives device context after reconnects
  • src/server/ws-router.ts — stores ClientContext per-connection on ws.data and threads it into agent.send
  • src/server/agent.ts — builds CURRENT_USER_DEVICE / CURRENT_USER_DEVICE_LABEL env vars and a system-prompt snippet that prevents the AI from shelling out just to read those values
  • src/server/codex-app-server.ts — propagates env overrides to the spawned Codex process and developer_instructions to turn/start; uses an envSignature to avoid unnecessary session restarts

Issues found:

  • The "does not ping on fresh connection" test in socket.test.ts was weakened: it dropped expect(ws.sent).toHaveLength(0) in favour of only checking ws.sent[0], so a regression that also emits a spurious ping would go undetected
  • buildRuntimeContextInstruction tells the AI to respond to both CURRENT_USER_DEVICE and CURRENT_USER_DEVICE_LABEL with the same currentUserDevice tag, even though the label field can differ; harmless today (both are equal in detectClientContext) but inconsistent with the interface contract

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 quality/test improvements with no runtime impact.

The device-context propagation pipeline is correct end-to-end: detection, transmission, storage, env injection, and system-prompt injection all work as intended. The two findings are (1) a weakened test assertion that loses coverage on ping-absence, and (2) a minor system-prompt inconsistency that has no observable effect today because the label always equals the device tag. Neither constitutes a present defect on the changed path.

src/client/app/socket.test.ts — the 'does not ping' test should assert ws.sent has exactly one message to preserve the original test's intent.

Important Files Changed

Filename Overview
src/client/lib/current-user-device.ts New UA-detection module with correct Pixel → Android ordering; normalizedUserAgent safely concatenates all three sources and trims whitespace.
src/client/app/socket.ts Sends system.setClientContext immediately after each WebSocket open event, ensuring the server always has fresh device context even after reconnects.
src/server/ws-router.ts Stores clientContext on per-connection ws.data and threads it into agent.send; handler is correctly scoped to the connection lifecycle.
src/server/agent.ts Injects device context as env vars and a system-prompt snippet; system prompt inconsistently uses only currentUserDevice when answering CURRENT_USER_DEVICE_LABEL queries.
src/server/codex-app-server.ts Adds envSignature for session-reuse detection and threads developerInstructions into collaboration_mode.settings; env signature uses JSON.stringify which is key-order-dependent but stable for the fixed output of buildAgentEnv.
src/client/app/socket.test.ts The 'does not ping on fresh connection' test now only asserts ws.sent[0] without checking the total message count, losing the guarantee that no spurious ping was sent.
src/client/lib/current-user-device.test.ts Good coverage of Pixel, macOS, and detectClientContext; tests are focused and pass injected navigator values cleanly.
src/server/ws-router.test.ts New integration test correctly verifies that clientContext is stored and forwarded to agent.send after a setClientContext command.
src/server/codex-app-server.test.ts New test verifies developer_instructions is propagated into turn/start collaboration mode settings; fake process correctly simulates the full handshake.
src/shared/protocol.ts Adds CurrentUserDevice union type, ClientContext interface, and system.setClientContext command — clean protocol extension with no issues.

Sequence Diagram

sequenceDiagram
    participant Browser as Client (Browser)
    participant Socket as KannaSocket
    participant WsRouter as ws-router (Server)
    participant Agent as AgentCoordinator
    participant Claude as Claude / Codex

    Browser->>Socket: WebSocket open
    Socket->>WsRouter: system.setClientContext { currentUserDevice: "pixel" }
    WsRouter->>WsRouter: ws.data.clientContext = context
    WsRouter-->>Socket: ack

    Browser->>Socket: chat.send { content: "..." }
    Socket->>WsRouter: chat.send command
    WsRouter->>Agent: send(command, ws.data.clientContext)
    Agent->>Agent: buildAgentEnv → CURRENT_USER_DEVICE=pixel
    Agent->>Agent: buildRuntimeContextInstruction → system prompt snippet
    Agent->>Claude: start turn (env overrides + system prompt)
    Claude-->>Agent: stream response
    Agent-->>WsRouter: ack + chatId
    WsRouter-->>Socket: ack
Loading

Reviews (1): Last reviewed commit: "fix: drop invalid ws-router protocol imp..." | Re-trigger Greptile

Comment on lines 160 to +171

await socket.ensureHealthyConnection()

expect(ws.sent).toHaveLength(0)
expect(ws.sent[0]).toMatchObject({
type: "command",
command: {
type: "system.setClientContext",
context: {
currentUserDevice: "pixel",
currentUserDeviceLabel: "pixel",
},
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "Does not ping" test no longer verifies the absence of a ping

The original assertion was expect(ws.sent).toHaveLength(0), which guaranteed that no messages were sent during ensureHealthyConnection() when the connection is fresh. The replacement only checks ws.sent[0] (the new setClientContext message) but never asserts that ws.sent[1] is absent. If a regression caused a spurious system.ping to be sent, this test would silently pass — defeating its purpose of confirming no ping occurs on a fresh connection.

Suggested change
await socket.ensureHealthyConnection()
expect(ws.sent).toHaveLength(0)
expect(ws.sent[0]).toMatchObject({
type: "command",
command: {
type: "system.setClientContext",
context: {
currentUserDevice: "pixel",
currentUserDeviceLabel: "pixel",
},
},
expect(ws.sent[0]).toMatchObject({
type: "command",
command: {
type: "system.setClientContext",
context: {
currentUserDevice: "pixel",
currentUserDeviceLabel: "pixel",
},
},
})
expect(ws.sent).toHaveLength(1)

Comment on lines +79 to +80
`- If the user asks for CURRENT_USER_DEVICE or CURRENT_USER_DEVICE_LABEL, answer with \`${currentUserDevice}\` directly.`,
"- Do not run shell commands just to read CURRENT_USER_DEVICE or CURRENT_USER_DEVICE_LABEL.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 System prompt tells AI to use device tag for both CURRENT_USER_DEVICE and CURRENT_USER_DEVICE_LABEL

The instruction on line 79 reads:

If the user asks for CURRENT_USER_DEVICE or CURRENT_USER_DEVICE_LABEL, answer with ${currentUserDevice} directly.

But CURRENT_USER_DEVICE_LABEL is a separate field (clientContext.currentUserDeviceLabel) and the two can differ. buildAgentEnv already handles this correctly by falling back through the label chain, yet the system prompt hard-codes the device tag as the answer for both variable names. Today detectClientContext sets them to the same value, so there's no observable bug — but if a future caller provides a richer label (e.g. "Pixel 8 Pro") the AI would still answer "pixel" for CURRENT_USER_DEVICE_LABEL, contradicting the actual env var.

Consider tracking both values in buildRuntimeContextInstruction:

function buildRuntimeContextInstruction(clientContext?: ClientContext) {
  const currentUserDevice = clientContext?.currentUserDevice ?? "unknown"
  const currentUserDeviceLabel =
    clientContext?.currentUserDeviceLabel ?? currentUserDevice
  return [
    "Runtime context:",
    "- You are running on the VPS host, not on the user's local device.",
    `- The user's current device tag is \`${currentUserDevice}\`.`,
    "- If you need to mention the current device, use that exact tag unless later tool output proves otherwise.",
    `- If the user asks for CURRENT_USER_DEVICE, answer with \`${currentUserDevice}\` directly.`,
    `- If the user asks for CURRENT_USER_DEVICE_LABEL, answer with \`${currentUserDeviceLabel}\` directly.`,
    "- Do not run shell commands just to read CURRENT_USER_DEVICE or CURRENT_USER_DEVICE_LABEL.",
  ].join("\n")
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant