remove per-agent connection polling

Agent connection state is now derived from session.connected in session/idx,
updated via refreshSessions(). No longer spawns N processes for N agents.

- Remove m_agentConnections hash and per-agent NinePConnection instances
- Remove readAgentRecords() (dead code - agents come from session/idx)
- reconcileAgentConnections() now just updates m_agentConnected from m_sessions

This eliminates one ollie-9p process per agent, leaving only:
- 1 daemon eventwait stream (for all events)
- 1 chat stream (for active agent only)
- 1 state stream (for active agent only)
This commit is contained in:
Levi Neely 2026-08-03 20:10:49 +02:00
parent 0e9a7016a3
commit 8a3d52965e
2 changed files with 24 additions and 73 deletions

View File

@ -142,24 +142,6 @@ QString Ollie9pClient::agentKey(const QString &sessionId, const QString &agentId
return sessionId + "\n" + agentId;
}
QList<Ollie9pClient::AgentRecord> Ollie9pClient::readAgentRecords(const QString &sessionId)
{
QList<AgentRecord> result;
if (sessionId.isEmpty()) return result;
// Use immutable ID — the 9P namespace resolves it via alias.
const QString raw = QString::fromUtf8(run9p({"ls", "session/" + sessionId + "/agent"})).trimmed();
for (const QString &name : raw.split('\n', Qt::SkipEmptyParts)) {
if (name == "new") continue;
const QString path = "session/" + sessionId + "/agent/" + name;
const QString id = QString::fromUtf8(run9p({"read", path + "/id"})).trimmed();
AgentRecord r;
r.id = id.isEmpty() ? name : id;
r.name = name;
result.append(r);
}
return result;
}
void Ollie9pClient::setDaemonConnected(bool connected)
{
if (m_daemonConnected == connected) return;
@ -229,71 +211,49 @@ void Ollie9pClient::setAgentConnected(const QString &key, bool connected)
void Ollie9pClient::startAgentConnections()
{
reconcileAgentConnections();
// Agent connection state now comes from session/idx (refreshSessions)
// and eventwait events. No per-agent processes needed.
// Just start streams for the active agent if we have one.
if (!m_activeSessionId.isEmpty() && !m_agentId.isEmpty()) {
startActiveAgentStreams();
}
}
// reconcileAgentConnections ensures one NinePConnection per known agent.
// Connections use immutable session/agent IDs in their 9P paths. The server
// namespace lists directories by mutable display name, but accepts the
// immutable UUID as an alias during path walks. This means connection paths
// remain valid across session/agent renames without reconnection.
// reconcileAgentConnections updates agent connected state from m_sessions.
// Connection state is derived from session.connected in session/idx.
void Ollie9pClient::reconcileAgentConnections()
{
if (!m_daemonConnected) return;
QSet<QString> wanted;
// Update connected state for all agents based on session data
for (const QVariant &value : std::as_const(m_sessions)) {
const QVariantMap session = value.toMap();
const QString sid = session.value("id").toString();
const bool isConnected = session.value("connected").toBool();
for (const QVariant &agentValue : session.value("agents").toList()) {
const QVariantMap agent = agentValue.toMap();
const QString aid = agent.value("id").toString();
const QString key = agentKey(sid, aid);
wanted.insert(key);
if (m_agentConnections.contains(key)) continue;
auto *connection = new NinePConnection(this);
m_agentConnections.insert(key, connection);
connect(connection, &NinePConnection::connected, this, [this, key]() {
const bool reconnected = m_seenAgentConnections.contains(key);
m_seenAgentConnections.insert(key);
setAgentConnected(key, true);
if (key == agentKey(m_activeSessionId, m_agentId)) {
if (reconnected)
emit sessionConnected();
bool wasConnected = m_agentConnected.value(key, false);
m_agentConnected[key] = isConnected;
// Emit signal if this is the active agent and connection state changed
if (key == agentKey(m_activeSessionId, m_agentId)) {
if (!wasConnected && isConnected) {
emit sessionConnected();
startActiveAgentStreams();
}
});
connect(connection, &NinePConnection::disconnected, this, [this, key]() {
// Agent may have been killed — refresh to get authoritative state
// rather than showing a transient red indicator.
refreshSessions();
});
// Retry with backoff (5s, max 3 retries) to avoid hammering the server
// when the connection file exists but returns an error (e.g., toolsConn nil).
connection->start(ollie9pBin(), {"-a", serverAddr(), "read", "--open-marker",
"session/" + sid + "/agent/" + aid + "/connection"}, 5000, true, 3);
}
}
}
for (auto it = m_agentConnections.begin(); it != m_agentConnections.end();) {
if (wanted.contains(it.key())) {
++it;
continue;
}
it.value()->disconnect(this); // prevent stale disconnect signals
it.value()->stop();
it.value()->deleteLater();
m_agentConnected.remove(it.key());
it = m_agentConnections.erase(it);
}
}
void Ollie9pClient::stopAgentConnections()
{
for (NinePConnection *connection : std::as_const(m_agentConnections)) {
connection->disconnect(this);
connection->stop();
}
qDeleteAll(m_agentConnections);
m_agentConnections.clear();
// No per-agent connections to stop anymore
m_agentConnected.clear();
}
// handleEvent processes a single event from eventwait.

View File

@ -74,13 +74,6 @@ public:
Q_INVOKABLE QString getConfig();
Q_INVOKABLE QStringList getAgents(const QString &sessionId);
struct AgentRecord {
QString id;
QString name;
bool connected = false;
};
QList<AgentRecord> readAgentRecords(const QString &sessionId);
void setAgentConnected(const QString &key, bool connected);
QString agentKey(const QString &sessionId, const QString &agentId) const;
Q_INVOKABLE void setActiveAgentId(const QString &agentId);
@ -141,9 +134,7 @@ private:
QVariantMap m_rootModels;
QString m_currentBackend;
bool m_daemonConnected = false;
QHash<QString, NinePConnection *> m_agentConnections;
QHash<QString, bool> m_agentConnected;
QSet<QString> m_seenAgentConnections;
QHash<QString, bool> m_agentConnected; // Derived from session.connected in session/idx
NinePConnection *m_daemon = nullptr;
// Agent state cache — updated via delta events