feat(FN-1634): promote mailbox to first-class navigation view
- Add MailboxView as a full-page navigation view replacing the modal-based approach - Migrate MessageStore from filesystem to SQLite backend for message persistence - Implement conversation grouping for inbox display with unread badge state - Remove modal plumbing (useModalManager, AppModals exports) and simplify App.tsx - Add evictStaleProcessing() to TriageProcessor for self-healing hung triage sessions - Add comprehensive MailboxView tests and Header mailbox tests - Update README documentation with MailboxView features - Add CSS styles for MailboxView component - Fix MobileNavBar tests for mailbox tab visibility
This commit is contained in:
@@ -276,6 +276,8 @@ export class TriageProcessor {
|
||||
/** The interval (ms) of the currently active `setInterval` timer. */
|
||||
private activePollMs: number | null = null;
|
||||
private processing = new Set<string>();
|
||||
/** Timestamps when tasks entered the `processing` set, for staleness detection. */
|
||||
private processingSince = new Map<string, number>();
|
||||
private wasGlobalPaused = false;
|
||||
private wasEnginePaused = false;
|
||||
/** Active agent sessions per task, used to terminate on pause. */
|
||||
@@ -403,6 +405,47 @@ export class TriageProcessor {
|
||||
return new Set(this.processing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum time a task can remain in the `processing` set before it's
|
||||
* considered stale (30 minutes). By this point the stuck detector
|
||||
* (default 20-min timeout) should have already killed the session
|
||||
* and the `finally` block should have cleaned up. If it hasn't,
|
||||
* the promise is hung (e.g., `promptWithFallback` never settled
|
||||
* after dispose) and self-healing recovery needs to force-evict it.
|
||||
*/
|
||||
private static readonly STALE_PROCESSING_THRESHOLD_MS = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Evict tasks from the `processing` set that have been there longer than
|
||||
* the staleness threshold. This handles the case where a stuck-kill
|
||||
* disposes the session but the `specifyTask` promise never settles
|
||||
* (hung `promptWithFallback`), leaving the task in `processing` forever
|
||||
* and blocking self-healing recovery.
|
||||
*
|
||||
* @returns the set of evicted task IDs
|
||||
*/
|
||||
evictStaleProcessing(): Set<string> {
|
||||
const now = Date.now();
|
||||
const threshold = TriageProcessor.STALE_PROCESSING_THRESHOLD_MS;
|
||||
const evicted = new Set<string>();
|
||||
|
||||
for (const [taskId, since] of this.processingSince) {
|
||||
if (now - since >= threshold) {
|
||||
triageLog.warn(
|
||||
`${taskId} has been in processing for ${Math.round((now - since) / 60_000)}min ` +
|
||||
`(threshold: ${Math.round(threshold / 60_000)}min) — evicting (likely hung promise)`,
|
||||
);
|
||||
this.processing.delete(taskId);
|
||||
this.processingSince.delete(taskId);
|
||||
this.activeSessions.delete(taskId);
|
||||
this.stuckAborted.delete(taskId);
|
||||
evicted.add(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
return evicted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover a triage task whose spec was already approved but the final
|
||||
* handoff out of `status: "specifying"` never completed.
|
||||
@@ -549,6 +592,7 @@ export class TriageProcessor {
|
||||
async specifyTask(task: Task): Promise<void> {
|
||||
if (this.processing.has(task.id)) return;
|
||||
this.processing.add(task.id);
|
||||
this.processingSince.set(task.id, Date.now());
|
||||
|
||||
triageLog.log(
|
||||
`Specifying ${task.id}: ${task.title || task.description.slice(0, 60)}`,
|
||||
@@ -896,6 +940,7 @@ export class TriageProcessor {
|
||||
}
|
||||
} finally {
|
||||
this.processing.delete(task.id);
|
||||
this.processingSince.delete(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user