mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
restore the activity stamp across restarts: the quiet ordering no longer flattens on deploy
Root cause of the reviewer's mass-bump measurement (17 of 17 sessions with an identical lastActivityAt): every restart restamps all sessions in the constructor loop, and the boot auto-attach's repaint re-bumps the rest within the same second. A 12-minute steady-state sample shows NO ambient mass bump, so restarts are the whole story, and Codeman restarts on every deploy. The stamp now has a display twin: recovery threads the previous run's lastActivityAt from state.json into the wire-visible stamp (getter + toState), and a 15s settle window keeps the attach repaint from overwriting it. Real actions (input, task assignment, respawn) always write through. The private stamp keeps its boot-anchored semantics untouched, because the idle confirmation reads it as how long the pane has been quiet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+51
-10
@@ -135,6 +135,14 @@ const MUX_STARTUP_DELAY_MS = 300;
|
||||
/** Delay before declaring session idle after last output (2 seconds) */
|
||||
const IDLE_DETECTION_DELAY_MS = 2000;
|
||||
|
||||
// How long after construction a RECOVERED session's wire activity stamp keeps
|
||||
// its restored previous-run value. Recovery attaches every pane at boot and the
|
||||
// attach repaint arrives as ordinary PTY output; without this window that
|
||||
// repaint would overwrite every restored stamp within the same second, which is
|
||||
// exactly the restart flattening the restore exists to prevent. Real actions
|
||||
// (input, task assignment, respawn) always stamp through it.
|
||||
const WIRE_ACTIVITY_SETTLE_MS = 15_000;
|
||||
|
||||
// Note: Auto-compact/clear timing constants moved to session-auto-ops.ts
|
||||
|
||||
/** Graceful shutdown delay when stopping session (100ms) */
|
||||
@@ -392,6 +400,12 @@ export class Session extends EventEmitter {
|
||||
private _textOutput = new BufferAccumulator(MAX_TEXT_OUTPUT_SIZE, TEXT_OUTPUT_TRIM_SIZE);
|
||||
private _errorBuffer: string = '';
|
||||
private _lastActivityAt: number;
|
||||
// Display twin of _lastActivityAt, reported by toState()/the getter. It can
|
||||
// lag behind on recovery: the restored previous-run stamp survives the attach
|
||||
// repaint (see _markActivity), so a restart does not flatten the home
|
||||
// screens' quiet ordering. Idle detection never reads it.
|
||||
private _wireActivityAt: number;
|
||||
private _wireActivitySettleUntil: number;
|
||||
private _claudeSessionId: string | null = null;
|
||||
private _totalCost: number = 0;
|
||||
private _messages: ClaudeMessage[] = [];
|
||||
@@ -592,6 +606,8 @@ export class Session extends EventEmitter {
|
||||
attachmentHistory?: SessionAttachmentHistoryItem[];
|
||||
/** Restored wall-clock ms of the pane's last Enter (see `lastSubmitAt`). */
|
||||
lastSubmitAt?: number;
|
||||
/** Restored wall-clock ms of the pane's last output (recovery only; see `_wireActivityAt`). */
|
||||
lastActivityAt?: number;
|
||||
/** Remote execution metadata for sessions launched through SSH inside local tmux. */
|
||||
remote?: SessionRemote;
|
||||
/** Docker execution metadata for sessions launched inside a container via local tmux. */
|
||||
@@ -620,9 +636,18 @@ export class Session extends EventEmitter {
|
||||
// NOW, not `createdAt`: recovery passes the ORIGINAL creation time of a
|
||||
// days-old tmux session, and seeding last-activity from it would report a
|
||||
// freshly re-attached pane as having been silent for days, which the idle
|
||||
// confirmation reads as "already quiet" and the home screens print as its
|
||||
// idle duration. For a genuinely new session the two are the same instant.
|
||||
// confirmation reads as "already quiet". For a genuinely new session the
|
||||
// two are the same instant.
|
||||
this._lastActivityAt = Date.now();
|
||||
// The WIRE copy of the stamp is allowed to be older: recovery threads the
|
||||
// previous run's value so a restart does not flatten the home screens'
|
||||
// most-recently-quiet ordering (every stamp otherwise resets to boot time,
|
||||
// and the attach repaint re-bumps the rest within the same second). The
|
||||
// settle window in _markActivity() carries the restored value through that
|
||||
// repaint; the private stamp above stays boot-anchored because the idle
|
||||
// confirmation reads it as "how long has the pane been quiet".
|
||||
this._wireActivityAt = config.lastActivityAt || Date.now();
|
||||
this._wireActivitySettleUntil = config.lastActivityAt ? Date.now() + WIRE_ACTIVITY_SETTLE_MS : 0;
|
||||
// Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one.
|
||||
this._claudeSessionId = config.resumeSessionId || this.id;
|
||||
// Restored from state.json on boot recovery. start() resets _claudeSessionId
|
||||
@@ -794,7 +819,21 @@ export class Session extends EventEmitter {
|
||||
}
|
||||
|
||||
get lastActivityAt(): number {
|
||||
return this._lastActivityAt;
|
||||
return this._wireActivityAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp activity NOW. The private stamp (idle detection's "how long has the
|
||||
* pane been quiet") always moves; the wire stamp holds its restored value
|
||||
* through the post-recovery attach-repaint window unless the activity is a
|
||||
* real action (input, task assignment, respawn), which always writes through.
|
||||
*/
|
||||
private _markActivity(realAction = false): void {
|
||||
this._lastActivityAt = Date.now();
|
||||
if (realAction || Date.now() >= this._wireActivitySettleUntil) {
|
||||
this._wireActivityAt = this._lastActivityAt;
|
||||
this._wireActivitySettleUntil = 0;
|
||||
}
|
||||
}
|
||||
|
||||
get claudeSessionId(): string | null {
|
||||
@@ -1219,7 +1258,9 @@ export class Session extends EventEmitter {
|
||||
parentSessionId: this._parentSessionId,
|
||||
currentTaskId: this._currentTaskId,
|
||||
createdAt: this.createdAt,
|
||||
lastActivityAt: this._lastActivityAt,
|
||||
// The wire twin, not the private stamp: it survives the post-recovery
|
||||
// attach repaint, so the home screens' quiet ordering survives a restart.
|
||||
lastActivityAt: this._wireActivityAt,
|
||||
name: this._name,
|
||||
mode: this.mode,
|
||||
autoClearEnabled: this._autoOps.autoClearEnabled,
|
||||
@@ -1585,7 +1626,7 @@ export class Session extends EventEmitter {
|
||||
|
||||
// BufferAccumulator handles auto-trimming when max size exceeded
|
||||
this._terminalBuffer.append(data);
|
||||
this._lastActivityAt = Date.now();
|
||||
this._markActivity();
|
||||
this.emit('terminal', data);
|
||||
this.emit('output', data);
|
||||
}
|
||||
@@ -2484,7 +2525,7 @@ export class Session extends EventEmitter {
|
||||
this._messages = [];
|
||||
this._lineBuffer = '';
|
||||
this._altScreenSeqCarry = '';
|
||||
this._lastActivityAt = Date.now();
|
||||
this._markActivity(true);
|
||||
}
|
||||
|
||||
private _clearAllTimers(): void {
|
||||
@@ -3083,7 +3124,7 @@ export class Session extends EventEmitter {
|
||||
// Legacy method for sending input - wraps runPrompt
|
||||
async sendInput(input: string): Promise<void> {
|
||||
this._status = 'busy';
|
||||
this._lastActivityAt = Date.now();
|
||||
this._markActivity(true);
|
||||
this.runPrompt(input).catch((err) => {
|
||||
const errorMsg = getErrorMessage(err);
|
||||
// Clean up task state so the task queue doesn't get stuck
|
||||
@@ -3091,7 +3132,7 @@ export class Session extends EventEmitter {
|
||||
const taskId = this._currentTaskId;
|
||||
this._currentTaskId = null;
|
||||
this._status = 'idle';
|
||||
this._lastActivityAt = Date.now();
|
||||
this._markActivity(true);
|
||||
this.emit('taskError', taskId, errorMsg);
|
||||
} else {
|
||||
this._status = 'idle';
|
||||
@@ -3252,13 +3293,13 @@ export class Session extends EventEmitter {
|
||||
this._textOutput.clear();
|
||||
this._errorBuffer = '';
|
||||
this._messages = [];
|
||||
this._lastActivityAt = Date.now();
|
||||
this._markActivity(true);
|
||||
}
|
||||
|
||||
clearTask(): void {
|
||||
this._currentTaskId = null;
|
||||
this._status = 'idle';
|
||||
this._lastActivityAt = Date.now();
|
||||
this._markActivity(true);
|
||||
}
|
||||
|
||||
getOutput(): string {
|
||||
|
||||
@@ -2657,6 +2657,11 @@ export class WebServer extends EventEmitter {
|
||||
// the launch conversation until the user types again, even though
|
||||
// the re-attached CLI is on a post-`/clear` one.
|
||||
lastSubmitAt: savedState?.lastSubmitAt,
|
||||
// The pane's last output, previous run's value. Without it every
|
||||
// restart restamped all sessions "now" (constructor + the attach
|
||||
// repaint within the same second), flattening the home screens'
|
||||
// most-recently-quiet ordering to tab order after each deploy.
|
||||
lastActivityAt: savedState?.lastActivityAt,
|
||||
// Remote SSH metadata must round-trip on recovery: without it the
|
||||
// attach cwd falls back to the (nonexistent-locally) remote path and
|
||||
// respawn rebuilds a LOCAL command, breaking the pane and silently
|
||||
|
||||
Reference in New Issue
Block a user