Skip to content

DX/footgun: action proxy silently drops every argument after the first (positional args don't work) #49

Description

@MarcosBrendonDePaula

Problem

The React proxy returned by Live.use(Component, ...) only forwards the first argument to the server-side action. Any positional arguments after the first are silently dropped.

The action signature on the server still type-checks if you write async spawn(agent: string, cwd: string) — TypeScript doesn't know the proxy only forwards one arg, so the developer writes posicional code that compiles, runs, and silently misbehaves at runtime.

Reproduction

Server (LiveAgentList.ts):

export class LiveAgentList extends LiveComponent<...> {
  static publicActions = ["spawn"] as const

  // Looks fine. Compiles. Runs. Silently breaks at runtime.
  async spawn(agent: string, cwd: string) {
    console.log({ agent, cwd })
    // agent === "claude --foo"   ✅
    // cwd   === undefined        ❌  ← second arg got dropped
    ...
  }
}

Client:

const list = Live.use(LiveAgentList, {} as any)
await list.spawn("claude --foo", "/home/me/project")
//                ^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^
//                forwarded       silently dropped

The server receives agent = \"claude --foo\" and cwd = undefined, returning a spurious validation error ("cwd vazio") that's hard to trace back to the real cause.

Root cause

In @fluxstack/live-react/dist/index.js around L722:

```js
return async (payload) => {
const id = componentId || lastComponentIdRef.current;
if (!id || !connected) throw new Error("Not connected");
const response = await sendMessageAndWait({
type: "CALL_ACTION",
componentId: id,
action: prop,
payload // ← only the first arg
}, 1e4);
...
};
```

And on the server (@fluxstack/live/dist/index.js L2972):

```js
const result = await method.call(component, payload); // single arg
```

So the contract is genuinely "actions take exactly one payload" — but that contract is invisible to TypeScript (the method signature on the class is whatever the developer wrote) and undocumented in the README I could find.

Why this is a footgun

  1. No compile-time error — the class method's signature is the source of truth for TS, so await list.spawn(a, b) is accepted.
  2. No runtime error — the dropped args just become undefined, which fails downstream with confusing messages ("cwd vazio", "id inválido", etc.).
  3. Easy to writeasync spawn(agent, cwd) is more idiomatic than async spawn({ agent, cwd }), so newcomers write it that way and hit the bug.
  4. Looks like a server bug — the developer sees "cwd is empty" on the server and chases the wrong file.

Suggested fixes (any of these would help)

  1. Document explicitly in the LiveComponent docs that actions receive a single object payload, with an example. (LLMD/resources/live-components.md)
  2. Type-level constraint: refine publicActions typing so the proxy's call signature is (payload: Parameters<Component[A]>[0]) => ... — that already constrains it to one argument, but ideally the method declaration itself should be constrained to one-arg via a helper like:
    ```ts
    type LiveAction<P, R> = (payload: P) => Promise
    ```
    and require actions to be declared as spawn: LiveAction<{ agent: string; cwd: string }, ...>.
  3. Dev-mode warning: when the React proxy is invoked with arguments.length > 1, console.warn a clear message: \"LiveComponent action '<name>' was called with N positional args but only the first is forwarded — actions take a single payload object. Did you mean .spawn({ agent, cwd })?\". Zero runtime cost in prod, catches the bug immediately in dev.

Option 3 is the lowest-risk and would have saved me a debugging session today. Happy to send a PR if you like the approach.

Environment

  • @fluxstack/live 0.8.0
  • @fluxstack/live-react 0.8.0
  • @fluxstack/live-client 0.8.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions