diff --git a/README.md b/README.md
index b0b66a0..d926bf3 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,47 @@
Ollie is an AI agent runtime built around a 9P virtual filesystem. The runtime service, `olliesrv`, exposes sessions, agents, prompts, state, history, tools, and control operations as files. Frontends remain thin clients of that interface.
+```mermaid
+flowchart TB
+ B["olliesrv
9P namespace · session lifecycle · agent runtime
prompt + history · backend dispatch"]
+
+ S1["Session A (toolsrv)
host + cwd"]
+ S2["Session B (toolsrv)
host + cwd"]
+ S3["Session C (toolsrv)
host + cwd"]
+ SX["(...)"]
+
+ B --- S1
+ B --- S2
+ B --- S3
+ B --- SX
+
+ A1["Agent 1"]
+ A2["Agent 2"]
+ A3["Agent 3"]
+ A4["Agent 4"]
+ A5["Agent 5"]
+ A6["Agent 6"]
+ AN["Agent N"]
+ AX["(...)"]
+
+ S1 --- A1
+ S1 --- A2
+ S2 --- A3
+ S2 --- A4
+ S3 --- A5
+ S3 --- A6
+ SX --- AN
+ SX --- AX
+
+ classDef brain fill:#d66b3d,stroke:#642b1c,color:#fff,stroke-width:4px;
+ classDef session fill:#e7c65c,stroke:#67551b,color:#211b08;
+ classDef agent fill:#c7d4eb,stroke:#354a70,color:#172033;
+
+ class B brain;
+ class S1,S2,S3,SX session;
+ class A1,A2,A3,A4,A5,A6,AN,AX agent;
+```
+
```text
frontends (shell, Acme, KDE, scripts)
│ 9P
diff --git a/kde/gui/chatblockmodel.cpp b/kde/gui/chatblockmodel.cpp
index 7f78e12..927e713 100644
--- a/kde/gui/chatblockmodel.cpp
+++ b/kde/gui/chatblockmodel.cpp
@@ -238,7 +238,6 @@ void ChatBlockModel::reset()
m_fenceContent.clear();
m_pendingDiagrams.clear();
m_inFlightRequests.clear();
- m_diagramCache.clear(); // Clear cache on reset to pick up fixes
endResetModel();
emit countChanged();
}
@@ -258,12 +257,6 @@ void ChatBlockModel::trimFront(int keepCount)
emit countChanged();
}
-void ChatBlockModel::clearDiagramCache()
-{
- m_diagramCache.clear();
- qDebug() << "Diagram cache cleared";
-}
-
QString ChatBlockModel::getDefaultTextColor() const
{
const auto textColor = QApplication::palette().color(QPalette::Text);
@@ -375,56 +368,10 @@ void ChatBlockModel::rebuildBlockHtml(int blockIndex)
html += diagramPlaceholder(fenceLang);
seg.diagramLoading = true;
} else {
- // Complete fence — check cache or request diagram
- const QString cacheKey = fenceLang + QStringLiteral(":") +
- QString::fromLatin1(QCryptographicHash::hash(
- code.toUtf8(), QCryptographicHash::Md5).toHex());
-
- if (m_diagramCache.contains(cacheKey)) {
- const QByteArray cached = m_diagramCache.value(cacheKey);
- if (cached.startsWith("ERROR:")) {
- // Error: fall back to source code rendering (not a diagram segment)
- QString codeHtml = highlightCode(code, fenceLang);
- html += codeHtml;
-
- BlockSegment textSeg;
- textSeg.type = BlockSegment::Text;
- textSeg.html = codeHtml;
- segments.append(textSeg);
-
- fenceLines.clear();
- fenceLang.clear();
- return; // Don't append diagram segment
- } else {
- // Check if JPEG, PNG, or SVG
- if (cached.startsWith("JPEG:")) {
- QByteArray jpegData = cached.mid(5); // Skip "JPEG:" prefix
- html += diagramJpegHtml(jpegData);
- seg.diagramSvg = QStringLiteral("data:image/jpeg;base64,") +
- QString::fromLatin1(jpegData.toBase64());
- } else if (cached.startsWith("PNG:")) {
- QByteArray pngData = cached.mid(4); // Skip "PNG:" prefix
- html += diagramPngHtml(pngData);
- seg.diagramSvg = QStringLiteral("data:image/png;base64,") +
- QString::fromLatin1(pngData.toBase64());
- } else if (cached.startsWith("SVG:")) {
- QByteArray svgData = cached.mid(4); // Skip "SVG:" prefix
- html += diagramSvgHtml(svgData);
- seg.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
- QString::fromLatin1(svgData.toBase64());
- } else {
- // Legacy: assume SVG if no prefix
- html += diagramSvgHtml(cached);
- seg.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
- QString::fromLatin1(cached.toBase64());
- }
- }
- } else {
- html += diagramPlaceholder(fenceLang);
- seg.diagramLoading = true;
- // Request diagram asynchronously
- requestDiagram(fenceLang, code, blockIndex);
- }
+ // Complete fence — render through the local diagram server.
+ html += diagramPlaceholder(fenceLang);
+ seg.diagramLoading = true;
+ requestDiagram(fenceLang, code, blockIndex);
}
segments.append(seg);
} else {
@@ -568,40 +515,15 @@ void ChatBlockModel::incrementalAppend(int blockIndex, const QString &line)
rs.inFence = false;
const QString code = rs.fenceLines.join('\n');
- if (isDiagramLanguage(rs.fenceLang)) {
- // Replace last segment with a diagram segment
- BlockSegment seg;
- seg.type = BlockSegment::Diagram;
- seg.diagramLang = rs.fenceLang;
- seg.diagramSource = code;
- seg.html = highlightCode(code, rs.fenceLang);
-
- const QString cacheKey = rs.fenceLang + QStringLiteral(":") +
- QString::fromLatin1(QCryptographicHash::hash(
- code.toUtf8(), QCryptographicHash::Md5).toHex());
-
- if (m_diagramCache.contains(cacheKey)) {
- const QByteArray cached = m_diagramCache.value(cacheKey);
- if (cached.startsWith("ERROR:")) {
- // Error: keep as text segment with highlighted source
- seg.type = BlockSegment::Text;
- } else if (cached.startsWith("JPEG:")) {
- seg.diagramSvg = QStringLiteral("data:image/jpeg;base64,") +
- QString::fromLatin1(cached.mid(5).toBase64());
- } else if (cached.startsWith("PNG:")) {
- seg.diagramSvg = QStringLiteral("data:image/png;base64,") +
- QString::fromLatin1(cached.mid(4).toBase64());
- } else if (cached.startsWith("SVG:")) {
- seg.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
- QString::fromLatin1(cached.mid(4).toBase64());
- } else {
- seg.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
- QString::fromLatin1(cached.toBase64());
- }
- } else {
+ if (isDiagramLanguage(rs.fenceLang)) {
+ // Replace last segment with a diagram segment
+ BlockSegment seg;
+ seg.type = BlockSegment::Diagram;
+ seg.diagramLang = rs.fenceLang;
+ seg.diagramSource = code;
+ seg.html = highlightCode(code, rs.fenceLang);
seg.diagramLoading = true;
requestDiagram(rs.fenceLang, code, blockIndex);
- }
// Replace the in-progress fence segment
if (!block.segments.isEmpty())
@@ -826,31 +748,22 @@ QString ChatBlockModel::diagramJpegHtml(const QByteArray &jpeg) const
void ChatBlockModel::requestDiagram(const QString &lang, const QString &code, int blockIndex)
{
- const QString cacheKey = lang + QStringLiteral(":") +
+ const QString requestKey = lang + QStringLiteral(":") +
QString::fromLatin1(QCryptographicHash::hash(
code.toUtf8(), QCryptographicHash::Md5).toHex());
- // Already have it cached?
- if (m_diagramCache.contains(cacheKey))
+ // Track duplicate requests only while this local render is in flight.
+ if (!m_pendingDiagrams.contains(requestKey))
+ m_pendingDiagrams[requestKey] = QList();
+ if (!m_pendingDiagrams[requestKey].contains(blockIndex))
+ m_pendingDiagrams[requestKey].append(blockIndex);
+ if (m_inFlightRequests.contains(requestKey))
return;
- // Track which blocks need this diagram
- if (!m_pendingDiagrams.contains(cacheKey)) {
- m_pendingDiagrams[cacheKey] = QList();
- }
- if (!m_pendingDiagrams[cacheKey].contains(blockIndex)) {
- m_pendingDiagrams[cacheKey].append(blockIndex);
- }
+ m_inFlightRequests.insert(requestKey);
- // Already requesting?
- if (m_inFlightRequests.contains(cacheKey))
- return;
-
- m_inFlightRequests.insert(cacheKey);
-
- // Build request
QNetworkRequest request;
- request.setAttribute(QNetworkRequest::User, cacheKey);
+ request.setAttribute(QNetworkRequest::User, requestKey);
request.setTransferTimeout(10000);
const QString langLower = lang.toLower();
@@ -867,7 +780,7 @@ void ChatBlockModel::requestDiagram(const QString &lang, const QString &code, in
request.setUrl(QUrl(QStringLiteral("http://localhost:8081/img/") + QString::fromLatin1(encoded)));
m_netManager->get(request);
} else {
- m_inFlightRequests.remove(cacheKey);
+ m_inFlightRequests.remove(requestKey);
return;
}
}
@@ -906,21 +819,75 @@ void ChatBlockModel::onDiagramReply(QNetworkReply *reply)
errorMsg = reply->errorString();
}
- // Update cache
if (success) {
- m_diagramCache[cacheKey] = imageData;
+ // Keep the result only on the waiting segments; do not cache diagrams.
} else {
- // Cache error as empty data with special marker
- m_diagramCache[cacheKey] = QByteArray("ERROR:") + errorMsg.toUtf8();
+ // The error is applied directly to waiting segments below.
}
- // Rebuild all blocks waiting for this diagram
+ // Apply the response directly to waiting segments. Nothing is retained after
+ // this request completes; a later occurrence renders through the local server.
const QList blocks = m_pendingDiagrams.take(cacheKey);
for (int blockIndex : blocks) {
- if (blockIndex < m_blocks.size()) {
- rebuildBlockHtml(blockIndex);
- QModelIndex idx = index(blockIndex);
- emit dataChanged(idx, idx, {RenderedHtmlRole, SegmentsRole});
+ if (blockIndex < 0 || blockIndex >= m_blocks.size())
+ continue;
+
+ ChatBlock &block = m_blocks[blockIndex];
+ BlockSegment *matchingSegment = nullptr;
+ for (BlockSegment &segment : block.segments) {
+ if (segment.type != BlockSegment::Diagram || !segment.diagramLoading)
+ continue;
+ const QString segmentKey = segment.diagramLang + QStringLiteral(":") +
+ QString::fromLatin1(QCryptographicHash::hash(
+ segment.diagramSource.toUtf8(), QCryptographicHash::Md5).toHex());
+ if (segmentKey == cacheKey) {
+ matchingSegment = &segment;
+ break;
+ }
}
+
+ // A block can be rebuilt while a request is in flight. In that case
+ // the segment still represents this request, but its source may have
+ // been normalized by the parser. Use the sole pending diagram rather
+ // than leaving the UI loading forever.
+ if (!matchingSegment) {
+ for (BlockSegment &segment : block.segments) {
+ if (segment.type == BlockSegment::Diagram && segment.diagramLoading) {
+ if (matchingSegment) {
+ matchingSegment = nullptr;
+ break;
+ }
+ matchingSegment = &segment;
+ }
+ }
+ }
+
+ if (!matchingSegment)
+ continue;
+
+ BlockSegment &segment = *matchingSegment;
+ segment.diagramLoading = false;
+ if (!success) {
+ segment.diagramError = errorMsg;
+ segment.html = diagramErrorHtml(errorMsg);
+ } else if (imageData.startsWith("JPEG:")) {
+ const QByteArray data = imageData.mid(5);
+ segment.diagramSvg = QStringLiteral("data:image/jpeg;base64,") +
+ QString::fromLatin1(data.toBase64());
+ segment.html = diagramJpegHtml(data);
+ } else if (imageData.startsWith("PNG:")) {
+ const QByteArray data = imageData.mid(4);
+ segment.diagramSvg = QStringLiteral("data:image/png;base64,") +
+ QString::fromLatin1(data.toBase64());
+ segment.html = diagramPngHtml(data);
+ } else {
+ const QByteArray data = imageData.startsWith("SVG:") ? imageData.mid(4) : imageData;
+ segment.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
+ QString::fromLatin1(data.toBase64());
+ segment.html = diagramSvgHtml(data);
+ }
+ rebuildRenderedHtmlFromSegments(block);
+ QModelIndex idx = index(blockIndex);
+ emit dataChanged(idx, idx, {RenderedHtmlRole, SegmentsRole});
}
}
diff --git a/kde/gui/chatblockmodel.h b/kde/gui/chatblockmodel.h
index 6a795e4..da75296 100644
--- a/kde/gui/chatblockmodel.h
+++ b/kde/gui/chatblockmodel.h
@@ -95,7 +95,6 @@ public:
Q_INVOKABLE void appendData(const QString &text);
Q_INVOKABLE void reset();
Q_INVOKABLE void trimFront(int keepCount);
- Q_INVOKABLE void clearDiagramCache();
signals:
void countChanged();
@@ -140,8 +139,6 @@ private:
QString m_fenceLanguage;
QString m_fenceContent;
- // Diagram cache: hash of (lang + code) -> SVG data
- QHash m_diagramCache;
// Pending requests: hash -> block indices waiting for this diagram
QHash> m_pendingDiagrams;
// Track which requests are in flight