fix: stale sessions permanently ignored + CLI missing custom post type
Two bugs fixed: 1. Session monitor stale session bug: Sessions that were stale on first poll got added to _knownSessions but never re-checked, even after their transcript became active. Now stale sessions are tracked separately in _staleSessions and re-checked on every poll cycle. 2. CLI live-status tool: create/update commands were creating plain text posts without the custom_livestatus post type or plugin props. The Mattermost webapp plugin only renders posts with type=custom_livestatus. Now all CLI commands set the correct post type and livestatus props.
This commit is contained in:
@@ -242,15 +242,37 @@ async function createPost(text, cmd) {
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
const agentName = options.agent || 'unknown';
|
||||
const statusMap = { create: 'running', update: 'running', complete: 'complete', error: 'error' };
|
||||
const cmdKey = cmd || 'create';
|
||||
let payload;
|
||||
if (options.rich) {
|
||||
payload = {
|
||||
channel_id: CONFIG.channel_id,
|
||||
message: '',
|
||||
props: { attachments: [buildAttachment(cmd || 'create', text)] },
|
||||
message: text,
|
||||
type: 'custom_livestatus',
|
||||
props: {
|
||||
attachments: [buildAttachment(cmdKey, text)],
|
||||
livestatus: {
|
||||
agent_id: agentName,
|
||||
status: statusMap[cmdKey] || 'running', // eslint-disable-line security/detect-object-injection
|
||||
lines: text.split('\n'),
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
payload = { channel_id: CONFIG.channel_id, message: text };
|
||||
payload = {
|
||||
channel_id: CONFIG.channel_id,
|
||||
message: text,
|
||||
type: 'custom_livestatus',
|
||||
props: {
|
||||
livestatus: {
|
||||
agent_id: agentName,
|
||||
status: statusMap[cmdKey] || 'running', // eslint-disable-line security/detect-object-injection
|
||||
lines: text.split('\n'),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (options.replyTo) payload.root_id = options.replyTo;
|
||||
const result = await request('POST', '/posts', payload);
|
||||
@@ -271,18 +293,26 @@ async function updatePost(postId, text, cmd) {
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
const agentName = options.agent || 'unknown';
|
||||
const statusMap = { create: 'running', update: 'running', complete: 'complete', error: 'error' };
|
||||
const cmdKey = cmd || 'update';
|
||||
const livestatusProps = {
|
||||
agent_id: agentName,
|
||||
status: statusMap[cmdKey] || 'running', // eslint-disable-line security/detect-object-injection
|
||||
lines: text.split('\n'),
|
||||
};
|
||||
|
||||
if (options.rich) {
|
||||
await request('PUT', `/posts/${postId}`, {
|
||||
id: postId,
|
||||
message: '',
|
||||
props: { attachments: [buildAttachment(cmd || 'update', text)] },
|
||||
message: text,
|
||||
props: { attachments: [buildAttachment(cmdKey, text)], livestatus: livestatusProps },
|
||||
});
|
||||
} else {
|
||||
const current = await request('GET', `/posts/${postId}`);
|
||||
await request('PUT', `/posts/${postId}`, {
|
||||
id: postId,
|
||||
message: text,
|
||||
props: current.props,
|
||||
props: { livestatus: livestatusProps },
|
||||
});
|
||||
}
|
||||
console.log('updated');
|
||||
|
||||
@@ -46,6 +46,8 @@ class SessionMonitor extends EventEmitter {
|
||||
|
||||
// Map<sessionKey, sessionEntry>
|
||||
this._knownSessions = new Map();
|
||||
// Set<sessionKey> — sessions that were skipped as stale; re-check on next poll
|
||||
this._staleSessions = new Set();
|
||||
// Cache: "user:XXXX" -> channelId (resolved DM channels)
|
||||
this._dmChannelCache = new Map();
|
||||
this._pollTimer = null;
|
||||
@@ -175,6 +177,8 @@ class SessionMonitor extends EventEmitter {
|
||||
if (dmIdx >= 0 && parts[dmIdx + 1]) {
|
||||
return parts[dmIdx + 1]; // eslint-disable-line security/detect-object-injection
|
||||
}
|
||||
// agent:main:mattermost:direct:USER_ID — DM sessions use "direct" prefix
|
||||
// Channel ID must be resolved via API (returns null here; resolveChannelFromEntry handles it)
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -343,9 +347,9 @@ class SessionMonitor extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Detect added sessions
|
||||
// Detect new or previously-stale sessions
|
||||
for (const [sessionKey, entry] of currentSessions) {
|
||||
if (!this._knownSessions.has(sessionKey)) {
|
||||
if (!this._knownSessions.has(sessionKey) || this._staleSessions.has(sessionKey)) {
|
||||
this._onSessionAdded(entry);
|
||||
}
|
||||
}
|
||||
@@ -354,6 +358,14 @@ class SessionMonitor extends EventEmitter {
|
||||
for (const [sessionKey] of this._knownSessions) {
|
||||
if (!currentSessions.has(sessionKey)) {
|
||||
this._onSessionRemoved(sessionKey);
|
||||
this._staleSessions.delete(sessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up stale entries for sessions no longer in sessions.json
|
||||
for (const sessionKey of this._staleSessions) {
|
||||
if (!currentSessions.has(sessionKey)) {
|
||||
this._staleSessions.delete(sessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,13 +386,16 @@ class SessionMonitor extends EventEmitter {
|
||||
}
|
||||
|
||||
// Skip stale sessions — only track if transcript was modified in last 5 minutes
|
||||
// This prevents creating status boxes for every old session in sessions.json
|
||||
// This prevents creating status boxes for every old session in sessions.json.
|
||||
// Stale sessions are tracked in _staleSessions and re-checked on every poll
|
||||
// so they get picked up as soon as the transcript becomes active again.
|
||||
try {
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
const stat = fs.statSync(transcriptFile);
|
||||
const ageMs = Date.now() - stat.mtimeMs;
|
||||
const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
||||
if (ageMs > STALE_THRESHOLD_MS) {
|
||||
this._staleSessions.add(sessionKey);
|
||||
if (this.logger) {
|
||||
this.logger.debug(
|
||||
{ sessionKey, ageS: Math.floor(ageMs / 1000) },
|
||||
@@ -390,7 +405,8 @@ class SessionMonitor extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
} catch (_e) {
|
||||
// File doesn't exist — skip silently
|
||||
// File doesn't exist — skip silently but track as stale for re-check
|
||||
this._staleSessions.add(sessionKey);
|
||||
if (this.logger) {
|
||||
this.logger.debug(
|
||||
{ sessionKey, transcriptFile },
|
||||
@@ -400,6 +416,9 @@ class SessionMonitor extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Session is fresh — remove from stale tracking
|
||||
this._staleSessions.delete(sessionKey);
|
||||
|
||||
// Sub-agents always pass through — they inherit parent channel via watcher-manager
|
||||
const isSubAgent = !!spawnedBy;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user