Skip to content
FaxterBook a diagnostic

Field journal

6 min read

The Voice Calling Saga — A Day With An AI Coding Assistant

Fourteen hours, six of them debugging, and one moment at 1:30 AM where the night turned around. What building with an AI assistant looks like when the easy answers run out.

  • Engineering

A field journal from the day Pulse got voice calling, May 5–6, 2026.

A break from the strategic essays this week. I have spent the last two days getting voice calling working on Pulse — the customer-support agent that lives at pulse.faxter.com, and the second module in the suite I have been writing about. The way the day actually went is, I think, a more useful artifact of what “vibe coding” looks like in practice than another piece of theory.

So this week, an interlude. A field journal. A real engineering arc that started with a question and ended at 1:30 AM with a hundred and fifty lines of replacement code that should not have had to exist.

Strategic essays resume next week.

Setup

It started with a single question: what do you think about adding voice call to Pulse? That kicked off about fourteen hours of dense collaboration with an AI coding assistant. Most of it productive. The last six hours a debugging marathon I won’t forget.

By the end, Pulse could receive WhatsApp voice calls, ring every available staff dashboard, and let an agent answer with their browser microphone. The customer hears them. They hear the customer. Real audio over a real WebRTC pipeline.

But the story is not really about voice calls. It is about how a human and an AI assistant work through a hard problem when the easy answers run out.

The Clean Half

The first eight hours felt like flow.

We started by thinking before building. Architecture, then UI, then a roadmap. The assistant pushed back on my initial impulse to dive in — let’s settle the open questions first, then the roadmap writes itself. It wasn’t wrong. We made decisions about transport (WebRTC for free, Twilio for paid features), about ringing behaviour (simultaneous, fastest finger), about transfer (in-browser staff transfer for both, WA-number transfer Twilio-only). Each one captured in a docs file with the why attached.

Then we shipped Phase 0 — the schema, the Call model, the webhook dispatch — in one tight commit. I placed a real call to my business number and we both watched the engine.io payload arrive in the logs. Field names matched. The plan worked.

Phase 1 was the part I’m proudest of. The work split cleanly into a backend half (Meta API client + answer/end wiring) and a frontend half (incoming-call toast + WebRTC SDP exchange + in-call panel). The assistant suggested we lock the contract surface between them — the call:incoming socket payload shape, the answer endpoint signature — and then dispatched two agents in parallel, each on its own git worktree. They came back roughly an hour later. The merges were clean because the contract was locked. That part was a small revelation: parallel AI work isn’t fragile if you treat the integration boundary like an API.

The Trouble Starts

It was about commit fifteen of the day when the first cracks appeared. A crash about timezone arithmetic. A 400 from Meta about a deprecated typing_indicator. Some 500s from the Baileys sidecar replaying buffered messages that included WhatsApp Status updates from status@broadcast.

Each of these was a real bug, none of them caused by our voice work directly, but all of them surfaced because our voice work created new sessions and new database states and new restart events that exposed latent issues. We fixed them one by one. The assistant pushed back when I said one was “new” — the constraint has been there since the initial migration, the bug is pre-existing. I pushed back harder — I was running this without errors before our changes. We were both kind of right. The bugs were latent; our changes activated them.

Each fix was small. Each one mattered. And each one was followed by “now real-time should work” — and it didn’t.

The Mystery

By midnight I had voice working in test calls but the dashboard wasn’t updating live. Messages would arrive in the database. The agent would reply. The customer would receive the reply. But on the staff screen, nothing — until I pressed F5.

The assistant and I went through a dozen hypotheses. None of them right.

  • It’s the auth token expiring. It wasn’t.
  • It’s WORKERS=4 without proper Redis fanout. We set it to 1; still broken.
  • It’s nginx misconfigured. I pasted my config; textbook correct.
  • It’s the Service Worker caching old JS. Tested in Incognito; same problem.
  • It’s the wss:// URL scheme tripping socket.io-client. Coerced to https://; same problem.
  • It’s the explicit transports config. Removed it; same problem.

Around hour three of this I made a decision that turned the whole night around. I asked the assistant to instrument the transport itself — to monkey-patch WebSocket.prototype.send at the very top of the entry point and log every byte that left the browser. Stop guessing. Show me the wire.

What I saw next is what I want to remember.

The browser sent 2probe (engine.io upgrade probe). It sent 5 (engine.io UPGRADE commit). It PONGed back when the server PINGed. It did everything except send the one packet that mattered: 40{"token":"..."} — the Socket.IO namespace-connect packet. The auth callback was firing — socket.io-client thought it was sending — but the bytes never made it to the wire.

To prove the wire wasn’t the problem, we did a brutal test: I pasted a raw WebSocket connection into my browser console, manually typed 40{"token":"..."} and called ws.send. The server received it. The connect handler ran. Everything that should have worked, worked — bypassing socket.io-client entirely.

That was the moment I understood: the library itself was lying about what it was doing.

The Pivot

There’s a cliff in any project where you stop trying to fix something and start trying to replace it. We were standing at that cliff at 1:30 AM. The assistant proposed it cleanly — Branch B4: write a raw-WebSocket adapter. About 150 lines. We knew the protocol; we’d just used it manually. There was no real reason to keep fighting socket.io-client.

I said yes.

The adapter went in fast. The assistant wrote it; I reviewed; we shipped. The first version had a small race condition — emits dispatched before the connect ack got dropped — and we fixed it by adopting socket.io-client’s own pattern: buffer pre-connect, flush on connect.

Then real-time worked.

A WhatsApp message arrived at the dashboard live. An incoming-call toast appeared the instant a call came in. Six hours of debugging ended in a single page reload that did everything it was supposed to do.

What I’m Taking From This

A few things I want to remember the next time I do this.

Plan before you build. The hour we spent on the architecture doc and the contract lock saved us at least three later. When the parallel agents merged cleanly, that was the dividend.

The assistant proposes; you verify. Several of my best moments in this session were pushing back. No, I really was running this without errors. No, that’s not enough evidence to commit a fix. Show me the actual diff. The assistant is excellent at making plausible hypotheses, and plausible hypotheses are not the same as true ones.

Instrument before you fix. Until I saw [ws.send] data=40{...} not appearing on the wire, every fix was a guess. The diagnostic patch was the real breakthrough; everything before it was theatre.

Bypass beats heroics. When socket.io-client wouldn’t cooperate after four config combinations and a version downgrade, the right move was not to keep configuring. It was to ship 150 lines of replacement code and stop depending on the broken thing.

Persist your findings. We saved memory entries — if real-time looks broken, check these three things in this order — so the next session doesn’t redebug from scratch. That’s a discipline, not a feature.

The marathon is part of the work. I almost gave up around hour four. The assistant didn’t, partly because it doesn’t get tired, partly because debugging is what it’s actually good at when you give it the right tools. There’s something useful about having a partner who’ll sit with you in the same problem at 1 AM and not flinch.

I’m going to bed. The dashboard is updating live. The voice toast pops up the moment a call rings. Customers can hear staff. Staff can hear customers.

Eight hundred lines of new code. Twenty-seven commits. One latent bug from a year ago, one new library written from scratch, and one really long night.

Worth it.

Faxter

We build the co-workers this is written about.