Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/system/user/server/modules/PersonaState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,17 @@ export const DEFAULT_STATE_CONFIG: StateConfig = {
/**
* PersonaStateManager: Manages internal state and traffic decisions
*/
/** Minimum interval between snapshot emissions per persona (ms) */
const SNAPSHOT_THROTTLE_MS = 2000;

Comment on lines 49 to +54

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SNAPSHOT_THROTTLE_MS constant was inserted between the class-level JSDoc (lines 49-51: PersonaStateManager: Manages internal state and traffic decisions) and the class PersonaStateManager declaration at line 55. In TypeScript/JSDoc, a /** */ comment documents the next declaration — so the class-level JSDoc now documents SNAPSHOT_THROTTLE_MS instead of the class, and the class itself loses its documentation. Move the constant above the class JSDoc or inside the class as a private static readonly to preserve the original documentation association.

Suggested change
/**
* PersonaStateManager: Manages internal state and traffic decisions
*/
/** Minimum interval between snapshot emissions per persona (ms) */
const SNAPSHOT_THROTTLE_MS = 2000;
/** Minimum interval between snapshot emissions per persona (ms) */
const SNAPSHOT_THROTTLE_MS = 2000;
/**
* PersonaStateManager: Manages internal state and traffic decisions
*/

Copilot uses AI. Check for mistakes.
export class PersonaStateManager {
private readonly config: StateConfig;
private state: PersonaState;
private readonly personaName: string;
private readonly personaId?: string;
private readonly logger?: SubsystemLogger;
private _lastSnapshotTime = 0;
private _snapshotPending = false;

constructor(personaName: string, config: Partial<StateConfig> = {}, personaId?: string) {
this.personaName = personaName;
Expand Down Expand Up @@ -268,17 +273,42 @@ export class PersonaStateManager {
* Uses DataDaemon.jtagContext for cross-context (server→browser) delivery.
* Without the context, bare Events.emit() stays server-local.
*/
/**
* Throttled snapshot emission — max once per SNAPSHOT_THROTTLE_MS.
* With 15 personas each calling this on every cycle (3-5s) plus rest(),
* unthrottled emission hit 200+/s and flooded the WebSocket to browser.
*/
Comment on lines 273 to +280

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two consecutive JSDoc blocks are stacked on emitSnapshot() — the original description (lines 271-275) and the new throttle rationale (lines 276-280). Only the last JSDoc block before a declaration is used by tooling (IDE tooltips, generated docs). The old block is now dead documentation. These should be merged into a single JSDoc comment that covers both the purpose (browser-side snapshot emission via jtagContext) and the throttling behavior.

Suggested change
* Uses DataDaemon.jtagContext for cross-context (server→browser) delivery.
* Without the context, bare Events.emit() stays server-local.
*/
/**
* Throttled snapshot emission max once per SNAPSHOT_THROTTLE_MS.
* With 15 personas each calling this on every cycle (3-5s) plus rest(),
* unthrottled emission hit 200+/s and flooded the WebSocket to browser.
*/
* Uses DataDaemon.jtagContext for cross-context (server→browser) delivery;
* without the context, bare Events.emit() stays server-local.
*
* Emission is throttled max once per SNAPSHOT_THROTTLE_MS. With many personas
* each calling this on every cycle (3–5s) plus rest(), unthrottled emission
* can reach 200+/s and flood the WebSocket to the browser.
*/

Copilot uses AI. Check for mistakes.
private emitSnapshot(): void {
if (!this.personaId) return;

const now = Date.now();
if (now - this._lastSnapshotTime < SNAPSHOT_THROTTLE_MS) {
// Schedule a trailing emit so the latest state always gets sent
if (!this._snapshotPending) {
this._snapshotPending = true;
setTimeout(() => {
this._snapshotPending = false;
this.emitSnapshotNow();
}, SNAPSHOT_THROTTLE_MS - (now - this._lastSnapshotTime));
}
return;
}

this.emitSnapshotNow();
Comment on lines +284 to +297

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a subtle edge case: when a leading-edge emit passes the throttle check while _snapshotPending is still true from a previously scheduled trailing timer, both the leading emit and the trailing timer will fire, causing a double-emit within milliseconds. This happens when setTimeout fires slightly late and a new call arrives just after the throttle window expires but before the pending timer runs.

To fix, either (a) store the timer ID and clear it when a leading emit fires, or (b) reset _snapshotPending = false at line 297 before calling this.emitSnapshotNow() in the leading path. Option (a) is more robust:

Store the timeout handle (e.g., private _snapshotTimer: ReturnType<typeof setTimeout> | null = null) and call clearTimeout(this._snapshotTimer) before the leading-edge emitSnapshotNow() call.

Copilot uses AI. Check for mistakes.
}

private emitSnapshotNow(): void {
if (!this.personaId) return;
this._lastSnapshotTime = Date.now();

const payload = {
personaId: this.personaId,
energy: this.state.energy,
attention: this.state.attention,
mood: this.state.mood,
inboxLoad: this.state.inboxLoad,
computeBudget: this.state.computeBudget,
timestamp: Date.now()
timestamp: this._lastSnapshotTime
};

const ctx = DataDaemon.jtagContext;
Expand Down
Loading