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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const agentName = options.agent || 'unknown';
|
||||||
|
const statusMap = { create: 'running', update: 'running', complete: 'complete', error: 'error' };
|
||||||
|
const cmdKey = cmd || 'create';
|
||||||
let payload;
|
let payload;
|
||||||
if (options.rich) {
|
if (options.rich) {
|
||||||
payload = {
|
payload = {
|
||||||
channel_id: CONFIG.channel_id,
|
channel_id: CONFIG.channel_id,
|
||||||
message: '',
|
message: text,
|
||||||
props: { attachments: [buildAttachment(cmd || 'create', 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 {
|
} 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;
|
if (options.replyTo) payload.root_id = options.replyTo;
|
||||||
const result = await request('POST', '/posts', payload);
|
const result = await request('POST', '/posts', payload);
|
||||||
@@ -271,18 +293,26 @@ async function updatePost(postId, text, cmd) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
try {
|
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) {
|
if (options.rich) {
|
||||||
await request('PUT', `/posts/${postId}`, {
|
await request('PUT', `/posts/${postId}`, {
|
||||||
id: postId,
|
id: postId,
|
||||||
message: '',
|
message: text,
|
||||||
props: { attachments: [buildAttachment(cmd || 'update', text)] },
|
props: { attachments: [buildAttachment(cmdKey, text)], livestatus: livestatusProps },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const current = await request('GET', `/posts/${postId}`);
|
|
||||||
await request('PUT', `/posts/${postId}`, {
|
await request('PUT', `/posts/${postId}`, {
|
||||||
id: postId,
|
id: postId,
|
||||||
message: text,
|
message: text,
|
||||||
props: current.props,
|
props: { livestatus: livestatusProps },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log('updated');
|
console.log('updated');
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class SessionMonitor extends EventEmitter {
|
|||||||
|
|
||||||
// Map<sessionKey, sessionEntry>
|
// Map<sessionKey, sessionEntry>
|
||||||
this._knownSessions = new Map();
|
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)
|
// Cache: "user:XXXX" -> channelId (resolved DM channels)
|
||||||
this._dmChannelCache = new Map();
|
this._dmChannelCache = new Map();
|
||||||
this._pollTimer = null;
|
this._pollTimer = null;
|
||||||
@@ -175,6 +177,8 @@ class SessionMonitor extends EventEmitter {
|
|||||||
if (dmIdx >= 0 && parts[dmIdx + 1]) {
|
if (dmIdx >= 0 && parts[dmIdx + 1]) {
|
||||||
return parts[dmIdx + 1]; // eslint-disable-line security/detect-object-injection
|
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;
|
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) {
|
for (const [sessionKey, entry] of currentSessions) {
|
||||||
if (!this._knownSessions.has(sessionKey)) {
|
if (!this._knownSessions.has(sessionKey) || this._staleSessions.has(sessionKey)) {
|
||||||
this._onSessionAdded(entry);
|
this._onSessionAdded(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,6 +358,14 @@ class SessionMonitor extends EventEmitter {
|
|||||||
for (const [sessionKey] of this._knownSessions) {
|
for (const [sessionKey] of this._knownSessions) {
|
||||||
if (!currentSessions.has(sessionKey)) {
|
if (!currentSessions.has(sessionKey)) {
|
||||||
this._onSessionRemoved(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
|
// 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 {
|
try {
|
||||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||||
const stat = fs.statSync(transcriptFile);
|
const stat = fs.statSync(transcriptFile);
|
||||||
const ageMs = Date.now() - stat.mtimeMs;
|
const ageMs = Date.now() - stat.mtimeMs;
|
||||||
const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
if (ageMs > STALE_THRESHOLD_MS) {
|
if (ageMs > STALE_THRESHOLD_MS) {
|
||||||
|
this._staleSessions.add(sessionKey);
|
||||||
if (this.logger) {
|
if (this.logger) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
{ sessionKey, ageS: Math.floor(ageMs / 1000) },
|
{ sessionKey, ageS: Math.floor(ageMs / 1000) },
|
||||||
@@ -390,7 +405,8 @@ class SessionMonitor extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} 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) {
|
if (this.logger) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
{ sessionKey, transcriptFile },
|
{ sessionKey, transcriptFile },
|
||||||
@@ -400,6 +416,9 @@ class SessionMonitor extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session is fresh — remove from stale tracking
|
||||||
|
this._staleSessions.delete(sessionKey);
|
||||||
|
|
||||||
// Sub-agents always pass through — they inherit parent channel via watcher-manager
|
// Sub-agents always pass through — they inherit parent channel via watcher-manager
|
||||||
const isSubAgent = !!spawnedBy;
|
const isSubAgent = !!spawnedBy;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user