Switch from delete+recreate to PUT in-place updates
- Removes flicker caused by delete+recreate pattern
- PUT updates modify post content in-place (smooth)
- Trade-off: Mattermost shows (edited) label, and PUT clears pin status
- Pin+PUT are incompatible in Mattermost API — every PUT clears is_pinned
- Fix pin API calls to use {} body instead of null
- Remove post-replaced event handler (no longer needed)
This commit is contained in:
@@ -163,40 +163,13 @@ class StatusBox extends EventEmitter {
|
|||||||
this.metrics.queueDepth = Math.max(0, this.metrics.queueDepth - 1);
|
this.metrics.queueDepth = Math.max(0, this.metrics.queueDepth - 1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Delete + recreate to keep status post at the bottom of the thread
|
// In-place PUT update — no delete+recreate, no flicker
|
||||||
// Mattermost clears pin on PUT, and edited posts stay at their original position
|
await this._apiCallWithRetry('PUT', `/api/v4/posts/${postId}`, {
|
||||||
const postInfo = this._postInfo.get(postId);
|
id: postId,
|
||||||
if (postInfo) {
|
message: this._truncate(text),
|
||||||
try {
|
});
|
||||||
await this._httpRequest('DELETE', `/api/v4/posts/${postId}`, null);
|
this.metrics.updatesSent++;
|
||||||
} catch (_delErr) {
|
resolvers.forEach(({ resolve }) => resolve());
|
||||||
// If delete fails, fall back to PUT
|
|
||||||
}
|
|
||||||
const body = { channel_id: postInfo.channelId, message: this._truncate(text) };
|
|
||||||
if (postInfo.rootId) body.root_id = postInfo.rootId;
|
|
||||||
const newPost = await this._httpRequest('POST', '/api/v4/posts', body);
|
|
||||||
// Update tracking
|
|
||||||
this._postInfo.set(newPost.id, postInfo);
|
|
||||||
this._postInfo.delete(postId);
|
|
||||||
// Update throttle state key
|
|
||||||
const newState = this._throttleState.get(postId);
|
|
||||||
if (newState) {
|
|
||||||
this._throttleState.delete(postId);
|
|
||||||
this._throttleState.set(newPost.id, { pending: null, timer: null, lastFiredAt: Date.now(), resolvers: [] });
|
|
||||||
}
|
|
||||||
// Notify caller of new post ID
|
|
||||||
this.emit('post-replaced', postId, newPost.id);
|
|
||||||
this.metrics.updatesSent++;
|
|
||||||
resolvers.forEach(({ resolve }) => resolve());
|
|
||||||
} else {
|
|
||||||
// Fallback: regular PUT update
|
|
||||||
await this._apiCallWithRetry('PUT', `/api/v4/posts/${postId}`, {
|
|
||||||
id: postId,
|
|
||||||
message: this._truncate(text),
|
|
||||||
});
|
|
||||||
this.metrics.updatesSent++;
|
|
||||||
resolvers.forEach(({ resolve }) => resolve());
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.metrics.updatesFailed++;
|
this.metrics.updatesFailed++;
|
||||||
resolvers.forEach(({ reject }) => reject(err));
|
resolvers.forEach(({ reject }) => reject(err));
|
||||||
|
|||||||
@@ -240,6 +240,14 @@ async function startDaemon() {
|
|||||||
const initialText = buildInitialText(agentId, sessionKey);
|
const initialText = buildInitialText(agentId, sessionKey);
|
||||||
postId = await sharedStatusBox.createPost(channelId, initialText, rootPostId);
|
postId = await sharedStatusBox.createPost(channelId, initialText, rootPostId);
|
||||||
logger.info({ sessionKey, postId, channelId }, 'Created status box');
|
logger.info({ sessionKey, postId, channelId }, 'Created status box');
|
||||||
|
|
||||||
|
// Auto-pin the status post so it's always visible
|
||||||
|
try {
|
||||||
|
await sharedStatusBox._apiCall('POST', '/api/v4/posts/' + postId + '/pin', {});
|
||||||
|
logger.debug({ sessionKey, postId }, 'Status post pinned');
|
||||||
|
} catch (pinErr) {
|
||||||
|
logger.warn({ sessionKey, err: pinErr }, 'Failed to pin status post');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ sessionKey, err }, 'Failed to create status post');
|
logger.error({ sessionKey, err }, 'Failed to create status post');
|
||||||
globalMetrics.lastError = err.message;
|
globalMetrics.lastError = err.message;
|
||||||
@@ -282,18 +290,6 @@ async function startDaemon() {
|
|||||||
logger.debug({ sessionKey }, 'Session removed from sessions.json');
|
logger.debug({ sessionKey }, 'Session removed from sessions.json');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- Post Replaced (delete+recreate) ----
|
|
||||||
sharedStatusBox.on('post-replaced', (oldPostId, newPostId) => {
|
|
||||||
// Update activeBoxes to point to new post ID
|
|
||||||
for (var entry of activeBoxes.entries()) {
|
|
||||||
if (entry[1].postId === oldPostId) {
|
|
||||||
entry[1].postId = newPostId;
|
|
||||||
logger.debug({ sessionKey: entry[0], oldPostId, newPostId }, 'Post replaced (delete+recreate)');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Session Update (from watcher) ----
|
// ---- Session Update (from watcher) ----
|
||||||
watcher.on('session-update', (sessionKey, state) => {
|
watcher.on('session-update', (sessionKey, state) => {
|
||||||
const box = activeBoxes.get(sessionKey);
|
const box = activeBoxes.get(sessionKey);
|
||||||
@@ -345,7 +341,7 @@ async function startDaemon() {
|
|||||||
|
|
||||||
// Unpin the status post when session is done
|
// Unpin the status post when session is done
|
||||||
try {
|
try {
|
||||||
await sharedStatusBox._apiCall('POST', `/api/v4/posts/${box.postId}/unpin`, null);
|
await sharedStatusBox._apiCall('POST', `/api/v4/posts/${box.postId}/unpin`, {});
|
||||||
logger.debug({ sessionKey, postId: box.postId }, 'Status post unpinned');
|
logger.debug({ sessionKey, postId: box.postId }, 'Status post unpinned');
|
||||||
} catch (unpinErr) {
|
} catch (unpinErr) {
|
||||||
logger.warn({ sessionKey, err: unpinErr }, 'Failed to unpin status post');
|
logger.warn({ sessionKey, err: unpinErr }, 'Failed to unpin status post');
|
||||||
|
|||||||
Reference in New Issue
Block a user