Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

ibetitsmike
Copy link
Contributor

@ibetitsmike ibetitsmike commented Aug 7, 2025

Added an "Immortal Streams" feature to the agent that maintains persistent TCP connections to local services, allowing clients to reconnect without losing data.

What changed?

  • Implemented a new immortalstreams package in the agent that provides persistent TCP connections to local services
  • Added a manager that handles stream creation, reconnection, and cleanup
  • Implemented a backed pipe mechanism that buffers data during client disconnections
  • Created REST API endpoints for creating, listing, and deleting streams
  • Added WebSocket support for client reconnections with sequence numbers to track data flow
  • Integrated the immortal streams manager into the agent lifecycle

Copy link
Contributor Author

ibetitsmike commented Aug 7, 2025

@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch from b81d8df to fa4eff3 Compare August 7, 2025 12:38
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-backed-base branch from 27da7ef to e5be506 Compare August 7, 2025 12:38
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch from fa4eff3 to 6e48486 Compare August 12, 2025 12:32
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-backed-base branch from e5be506 to 77e912f Compare August 12, 2025 12:32
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch 2 times, most recently from ea83092 to bae956a Compare August 12, 2025 21:21
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-backed-base branch from 16abe05 to dde9516 Compare August 12, 2025 21:21
@ibetitsmike ibetitsmike changed the title chore: add backed reader, writer and pipe chore: add immortal streams manager Aug 12, 2025
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch 2 times, most recently from 2fbfcb1 to b4276f8 Compare August 15, 2025 00:51
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-backed-base branch from 7468299 to b2188f9 Compare August 15, 2025 00:51
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch 3 times, most recently from 0b6e27e to 9bcaa2f Compare August 19, 2025 06:52
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-backed-base branch from 45558ec to 064514e Compare August 19, 2025 06:52
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch 2 times, most recently from e256a4a to 5c50940 Compare August 20, 2025 11:42
@ibetitsmike ibetitsmike marked this pull request as ready for review August 20, 2025 11:42
@ibetitsmike ibetitsmike requested review from spikecurtis and removed request for spikecurtis August 20, 2025 11:43
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch from 5c50940 to c0cba16 Compare August 20, 2025 12:38
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-backed-base branch from 95dc01a to 85c505d Compare August 20, 2025 12:38
@ibetitsmike ibetitsmike changed the base branch from mike/immortal-streams-backed-base to graphite-base/19225 August 25, 2025 20:19
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch from 57b8912 to fd99a7f Compare August 25, 2025 20:32
@github-actions github-actions bot added the stale This issue is like stale bread. label Sep 2, 2025
@github-actions github-actions bot closed this Sep 5, 2025
@ibetitsmike ibetitsmike reopened this Sep 8, 2025
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch 2 times, most recently from 4c46fea to e81618b Compare September 8, 2025 08:54
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch 2 times, most recently from 0f58377 to 2173bc4 Compare September 8, 2025 10:01
@ibetitsmike ibetitsmike changed the base branch from graphite-base/19225 to mike/immortal-streams-backed-base September 8, 2025 10:05
Base automatically changed from mike/immortal-streams-backed-base to main September 11, 2025 12:05
@ibetitsmike ibetitsmike removed the stale This issue is like stale bread. label Sep 12, 2025
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch from c00f439 to dafdb32 Compare September 12, 2025 12:55
@ibetitsmike ibetitsmike force-pushed the mike/immortal-streams-agent-api branch from dafdb32 to be330a4 Compare September 12, 2025 12:59
@ibetitsmike ibetitsmike changed the title chore: add immortal streams manager feat: add immortal streams manager Sep 12, 2025

// Always dial localhost; internal listeners are handled by the dialer.
addr := fmt.Sprintf("localhost:%d", port)
conn, err := m.dialer.DialContext(ctx, addr)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we are hard-coding the host portion of the address, it doesn't convey any information. Why not make the dialer just take the port?


// Always dial localhost; internal listeners are handled by the dialer.
addr := fmt.Sprintf("localhost:%d", port)
conn, err := m.dialer.DialContext(ctx, addr)
Copy link
Contributor

Choose a reason for hiding this comment

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

Holding the lock while dialing will prevent creation of any new streams, reconnection to old streams, etc.


disconnectedAt := stream.LastDisconnectionAt()

// Prioritize streams that have actually been disconnected over never-connected streams
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't this mean that if a client creates a stream and then fails to connect to it, that stream will never get evicted?

RFC says:

  • When a new stream is created that would put us over the limit, we transparently kill the oldest, disconnected Stream.
  • If all 32 Streams are currently connected, we refuse to evict and return an error on the create API call.

To me, this reads as prioritizing currently connected streams only. Nothing about never-connected vs connected-then-disconnected.

I see there is some ambiguity about what we mean by "oldest." What makes sense to me is to take the newest datetime among created_at, disconnected_at, and use that for a disconnected stream's age.

closed := stream.closed
handshaking := stream.handshakePending
streamDisconnected := !stream.connected
pipeDisconnected := stream.pipe != nil && !stream.pipe.Connected()
Copy link
Contributor

Choose a reason for hiding this comment

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

This would be a lot simpler if the pipe is never nil'd out. It's not clear to me why Stream.Close() needs to set the pipe to nil.

// Closing the channel wakes all waiters exactly once
select {
case <-s.shutdownChan:
// already closed
Copy link
Contributor

Choose a reason for hiding this comment

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

unhittable. You check whether we are already closed on line 319. Just close the channel without this select.

default:
// already requested; coalesced
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems unnecessarily complicated to have the stream signal the pipe to reconnect, which itself calls back into the stream to wait for a connection. It involves multiple chanels and a sync.Cond.

Instead, on the Agent side, the Reconnect callback can be a no-op. There is nothing to do on the agent side when the unreliable connection goes down.

Then, when a new connection comes in like this, directly call a method on the backed pipe to do the reconnection & replay.


// UpdateTailnetConn updates the tailnet connection and agent address.
// This allows the LocalDialer to start using tailscale network routing.
func (d *LocalDialer) UpdateTailnetConn(tailnetConn *tailnet.Conn) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This smells funny. The tailnet Conn should be part of initializing immortal streams in the first place, not added after the fact.

Immortal Streams are part of the Agent's HTTP API, which cannot be accessed until the tailnet network is initialized, so there should be no problem with strict dependencies at initialization.

// connection was delivered to it. If no internal listener exists for the port,
// it returns (nil, false, nil). If an unexpected error occurs while attempting
// to wire up the connection, an error is returned.
func (c *Conn) DialInternalTCP(_ context.Context, dstPort uint16) net.Conn {
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment lies about return values. This should return (net.Conn, error) and report a proper error instead of nil, partly for logging and partly for consistency of style.


server, client := net.Pipe()
// Deliver the server end to the listener asynchronously.
go handler(server)
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems strange to me that we don't wait for the handler --- this is the bit that actually connects to the listener. Waiting for the dial to complete is the usual behavior of a dialer.

logger slog.Logger

// localDialer handles traditional local network connections
localDialer *net.Dialer
Copy link
Contributor

Choose a reason for hiding this comment

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

confusing to have a field called localDialer on a struct called LocalDialer.

Do we even need this? It's always set to a zero-valued *net.Dialer, so you could just instantiate one directly when needed.

d := net.Dialer{}
return d.DialContext(ctx, "tcp", address)

Go automatically handles the pointer receiver.

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.

2 participants