mstro-app 0.5.1 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +9 -9
- package/README.md +71 -28
- package/bin/commands/config.js +1 -1
- package/bin/mstro.js +55 -4
- package/dist/server/cli/eta-estimator.d.ts +55 -0
- package/dist/server/cli/eta-estimator.d.ts.map +1 -0
- package/dist/server/cli/eta-estimator.js +222 -0
- package/dist/server/cli/eta-estimator.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +64 -9
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +19 -12
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.js +5 -1
- package/dist/server/cli/improvisation-history-store.js.map +1 -1
- package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
- package/dist/server/cli/improvisation-output-queue.js +30 -7
- package/dist/server/cli/improvisation-output-queue.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +29 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +50 -1
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +2 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/engines/EngineEvent.d.ts +126 -0
- package/dist/server/engines/EngineEvent.d.ts.map +1 -0
- package/dist/server/engines/EngineEvent.js +11 -0
- package/dist/server/engines/EngineEvent.js.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
- package/dist/server/engines/factory.d.ts +21 -0
- package/dist/server/engines/factory.d.ts.map +1 -0
- package/dist/server/engines/factory.js +152 -0
- package/dist/server/engines/factory.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
- package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
- package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
- package/dist/server/engines/opencode/model-catalog.js +141 -0
- package/dist/server/engines/opencode/model-catalog.js.map +1 -0
- package/dist/server/engines/types.d.ts +146 -0
- package/dist/server/engines/types.d.ts.map +1 -0
- package/dist/server/engines/types.js +4 -0
- package/dist/server/engines/types.js.map +1 -0
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +8 -124
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +45 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +69 -5
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/factory.d.ts +70 -0
- package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
- package/dist/server/mcp/classifier/factory.js +155 -0
- package/dist/server/mcp/classifier/factory.js.map +1 -0
- package/dist/server/services/plan/agent-resolver.d.ts +26 -0
- package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
- package/dist/server/services/plan/agent-resolver.js +102 -0
- package/dist/server/services/plan/agent-resolver.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +59 -11
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +33 -1
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/settings.d.ts +76 -2
- package/dist/server/services/settings.d.ts.map +1 -1
- package/dist/server/services/settings.js +127 -4
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.js +19 -6
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +17 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +54 -2
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-complexity.js +78 -26
- package/dist/server/services/websocket/quality-complexity.js.map +1 -1
- package/dist/server/services/websocket/quality-eta.d.ts +47 -0
- package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-eta.js +110 -0
- package/dist/server/services/websocket/quality-eta.js.map +1 -0
- package/dist/server/services/websocket/quality-grading.d.ts +27 -4
- package/dist/server/services/websocket/quality-grading.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-grading.js +369 -201
- package/dist/server/services/websocket/quality-grading.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +145 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-operations.d.ts +34 -0
- package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-operations.js +47 -0
- package/dist/server/services/websocket/quality-operations.js.map +1 -0
- package/dist/server/services/websocket/quality-persistence.d.ts +9 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +10 -0
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +105 -56
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +9 -1
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +334 -14
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +21 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +49 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +35 -2
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js +1 -1
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +3 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +57 -9
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.js +3 -0
- package/dist/server/services/websocket/session-history.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +158 -42
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +25 -0
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +19 -0
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +35 -4
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.js +10 -2
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.js +138 -12
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.js +55 -2
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +47 -2
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +28 -5
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +10 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +5 -3
- package/server/cli/eta-estimator.ts +249 -0
- package/server/cli/headless/stall-assessor.ts +93 -0
- package/server/cli/headless/tool-watchdog.ts +21 -0
- package/server/cli/improvisation-history-store.ts +4 -1
- package/server/cli/improvisation-output-queue.ts +29 -7
- package/server/cli/improvisation-session-manager.ts +54 -1
- package/server/cli/improvisation-types.ts +2 -0
- package/server/engines/EngineEvent.ts +156 -0
- package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
- package/server/engines/factory.ts +176 -0
- package/server/engines/opencode/OpenCodeEngine.ts +786 -0
- package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
- package/server/engines/opencode/model-catalog.ts +217 -0
- package/server/engines/types.ts +173 -0
- package/server/index.ts +1 -1
- package/server/mcp/bouncer-haiku.ts +21 -145
- package/server/mcp/bouncer-integration.ts +107 -5
- package/server/mcp/classifier/BouncerClassifier.ts +40 -0
- package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
- package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
- package/server/mcp/classifier/factory.ts +195 -0
- package/server/services/plan/agent-resolver.ts +115 -0
- package/server/services/plan/agents/code-review.md +38 -8
- package/server/services/plan/composer.ts +63 -11
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/issue-prompt-builder.ts +39 -1
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/types.ts +4 -0
- package/server/services/settings.ts +161 -4
- package/server/services/websocket/git-branch-handlers.ts +20 -6
- package/server/services/websocket/handler.ts +59 -2
- package/server/services/websocket/quality-complexity.ts +80 -26
- package/server/services/websocket/quality-eta.ts +155 -0
- package/server/services/websocket/quality-grading.ts +445 -222
- package/server/services/websocket/quality-handlers.ts +153 -7
- package/server/services/websocket/quality-operations.ts +72 -0
- package/server/services/websocket/quality-persistence.ts +17 -0
- package/server/services/websocket/quality-review-agent.ts +154 -64
- package/server/services/websocket/quality-service.ts +361 -13
- package/server/services/websocket/quality-tools.ts +51 -0
- package/server/services/websocket/quality-types.ts +41 -2
- package/server/services/websocket/session-handlers.ts +64 -10
- package/server/services/websocket/session-history.ts +3 -0
- package/server/services/websocket/session-initialization.ts +189 -46
- package/server/services/websocket/session-registry.ts +37 -0
- package/server/services/websocket/settings-handlers.ts +41 -4
- package/server/services/websocket/tab-broadcast.ts +10 -2
- package/server/services/websocket/tab-event-buffer.ts +143 -11
- package/server/services/websocket/tab-event-replay.ts +70 -3
- package/server/services/websocket/tab-handlers.ts +53 -5
- package/server/services/websocket/types.ts +37 -5
|
@@ -39,20 +39,57 @@ export interface BufferedEvent {
|
|
|
39
39
|
data: unknown;
|
|
40
40
|
/** `Date.now()` at record time. Used for age-based eviction. */
|
|
41
41
|
timestamp: number;
|
|
42
|
+
/**
|
|
43
|
+
* Approximate serialized byte size of `data`. Computed once at record
|
|
44
|
+
* time so eviction can enforce a memory cap without re-stringifying on
|
|
45
|
+
* every check. Type and seq overhead is small; we only bill `data` here.
|
|
46
|
+
*/
|
|
47
|
+
byteSize: number;
|
|
42
48
|
}
|
|
43
49
|
/**
|
|
44
50
|
* Bounded replay log for a single tab.
|
|
45
51
|
*
|
|
46
|
-
* Size/age limits are parameterised for testability but defaulted to
|
|
47
|
-
* that comfortably cover real-world reconnect windows
|
|
52
|
+
* Size/age/byte limits are parameterised for testability but defaulted to
|
|
53
|
+
* values that comfortably cover real-world reconnect windows for long-running
|
|
54
|
+
* coding-agent tasks (multi-tool, multi-minute).
|
|
55
|
+
*
|
|
56
|
+
* ## Replay-gap detection
|
|
57
|
+
*
|
|
58
|
+
* The buffer tracks `evictedThroughSeq` — the highest seq that has ever been
|
|
59
|
+
* evicted (0 if nothing has been evicted). A web client whose `lastSeenSeq`
|
|
60
|
+
* is below this value has missed events the buffer can no longer supply, and
|
|
61
|
+
* an incremental replay would produce a silent gap. Callers should consult
|
|
62
|
+
* `hasGapSince` before relying on `getSince` for incremental replay; on a
|
|
63
|
+
* gap they should fall back to a full snapshot path (e.g. `outputHistory`).
|
|
64
|
+
*
|
|
65
|
+
* ## Eviction is FIFO with three caps
|
|
66
|
+
*
|
|
67
|
+
* Events are evicted from the front when ANY of these limits is exceeded:
|
|
68
|
+
* - count: `maxEvents` (default 10k)
|
|
69
|
+
* - age: `maxAgeMs` (default 60 min)
|
|
70
|
+
* - bytes: `maxTotalBytes` (default 32 MB)
|
|
71
|
+
*
|
|
72
|
+
* The byte cap is the safety belt against pathological events (e.g. a 50 MB
|
|
73
|
+
* grep result streamed as one event). Without it, count- and age-based caps
|
|
74
|
+
* still allow a single tab to hoard arbitrary memory.
|
|
48
75
|
*/
|
|
49
76
|
export declare class TabEventBuffer {
|
|
50
77
|
private readonly maxEvents;
|
|
51
78
|
private readonly maxAgeMs;
|
|
52
79
|
private readonly now;
|
|
80
|
+
private readonly maxTotalBytes;
|
|
53
81
|
private readonly events;
|
|
54
82
|
private nextSeq;
|
|
55
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Highest seq that has been evicted from the buffer. 0 means nothing has
|
|
85
|
+
* been evicted yet (buffer is operating within its window). Monotonically
|
|
86
|
+
* non-decreasing — eviction always happens from the front of the FIFO, in
|
|
87
|
+
* seq order, so the most recently evicted seq is always the highest.
|
|
88
|
+
*/
|
|
89
|
+
private evictedThroughSeq;
|
|
90
|
+
/** Approximate sum of `byteSize` over still-resident events. */
|
|
91
|
+
private totalBytes;
|
|
92
|
+
constructor(maxEvents?: number, maxAgeMs?: number, now?: () => number, maxTotalBytes?: number);
|
|
56
93
|
/**
|
|
57
94
|
* Append an event and return its assigned sequence number.
|
|
58
95
|
*
|
|
@@ -65,18 +102,51 @@ export declare class TabEventBuffer {
|
|
|
65
102
|
* Return all still-buffered events with `seq > afterSeq`, in original
|
|
66
103
|
* order. Returns an empty array if nothing newer is buffered (either the
|
|
67
104
|
* web is caught up or the window has rolled past).
|
|
105
|
+
*
|
|
106
|
+
* NOTE: This does not detect or signal replay gaps. Pair with
|
|
107
|
+
* `hasGapSince(afterSeq)` to know whether a returned array is a complete
|
|
108
|
+
* incremental replay or a partial one (events between `afterSeq` and the
|
|
109
|
+
* oldest surviving seq have been evicted and are no longer available).
|
|
68
110
|
*/
|
|
69
111
|
getSince(afterSeq: number): BufferedEvent[];
|
|
112
|
+
/**
|
|
113
|
+
* True when an incremental replay starting from `afterSeq` would silently
|
|
114
|
+
* skip events that the buffer has already evicted. Used by the replay
|
|
115
|
+
* orchestrator to decide whether to fall back to a full snapshot rather
|
|
116
|
+
* than emit a partial event stream the web can't reconstruct.
|
|
117
|
+
*
|
|
118
|
+
* `afterSeq < evictedThroughSeq` means the next event the caller expects
|
|
119
|
+
* (`afterSeq + 1`) is at or below the eviction frontier — that event has
|
|
120
|
+
* already been dropped from memory.
|
|
121
|
+
*/
|
|
122
|
+
hasGapSince(afterSeq: number): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Highest seq that has been evicted from this buffer; 0 if nothing has been
|
|
125
|
+
* evicted yet. Exposed for telemetry and gap-recovery decisions.
|
|
126
|
+
*/
|
|
127
|
+
getEvictedThroughSeq(): number;
|
|
70
128
|
/** Current highest assigned seq (monotonic; not reset by eviction). */
|
|
71
129
|
currentSeq(): number;
|
|
72
130
|
/** Events currently held in memory. For tests. */
|
|
73
131
|
size(): number;
|
|
132
|
+
/** Approximate bytes held by `data` payloads currently in memory. For tests/telemetry. */
|
|
133
|
+
byteSize(): number;
|
|
74
134
|
/**
|
|
75
135
|
* Drop events older than `maxAgeMs` from the front, then enforce
|
|
76
|
-
* `maxEvents` by trimming the front further if needed.
|
|
77
|
-
* newest events — they're the ones the web is most
|
|
136
|
+
* `maxEvents` and `maxTotalBytes` by trimming the front further if needed.
|
|
137
|
+
* Eviction keeps the newest events — they're the ones the web is most
|
|
138
|
+
* likely to still need.
|
|
139
|
+
*
|
|
140
|
+
* Each evicted seq advances `evictedThroughSeq` so callers can detect
|
|
141
|
+
* replay gaps. The FIFO ensures we always evict in seq order, so the last
|
|
142
|
+
* evicted seq is always the highest seen so far.
|
|
143
|
+
*
|
|
144
|
+
* The byte cap is enforced LAST so that count- and age-based eviction get
|
|
145
|
+
* a chance first; a chatty-but-small session evicts on age before it ever
|
|
146
|
+
* touches the byte cap, which keeps the usual case predictable.
|
|
78
147
|
*/
|
|
79
148
|
private evict;
|
|
149
|
+
private popOldest;
|
|
80
150
|
}
|
|
81
151
|
/**
|
|
82
152
|
* Registry of per-tab buffers. Kept as a thin collection so `HandlerContext`
|
|
@@ -96,8 +166,27 @@ export declare class TabEventBufferRegistry {
|
|
|
96
166
|
/** Drop all bookkeeping. Used for tests; no production caller expected. */
|
|
97
167
|
clear(): void;
|
|
98
168
|
}
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
169
|
+
/**
|
|
170
|
+
* 10,000 events per tab.
|
|
171
|
+
*
|
|
172
|
+
* Sized for long-running coding-agent tasks (multi-tool, multi-minute) plus
|
|
173
|
+
* laptop sleep/wake reconnect windows. Worst-case observed: a 14-minute
|
|
174
|
+
* session with ~120 tool calls produces ~1.5–3k tab-scoped events; 10× that
|
|
175
|
+
* gives headroom for parallel agents and chatty improvisation. Memory
|
|
176
|
+
* footprint at ~500B/event = ~5MB per tab; the local-only single-tenant
|
|
177
|
+
* deployment makes this a non-issue.
|
|
178
|
+
*/
|
|
179
|
+
export declare const DEFAULT_MAX_EVENTS = 10000;
|
|
180
|
+
/**
|
|
181
|
+
* 60 minutes of history. Covers laptop sleep/wake, long meetings between
|
|
182
|
+
* sessions, and the largest plausible reconnect window that a tab might
|
|
183
|
+
* legitimately want to recover incrementally instead of starting fresh.
|
|
184
|
+
*/
|
|
102
185
|
export declare const DEFAULT_MAX_AGE_MS: number;
|
|
186
|
+
/**
|
|
187
|
+
* 32 MB safety belt against pathological events (large grep results, full
|
|
188
|
+
* file reads streamed inline). Eviction by bytes guarantees a single tab
|
|
189
|
+
* can't hoard arbitrary memory regardless of count/age limits.
|
|
190
|
+
*/
|
|
191
|
+
export declare const DEFAULT_MAX_TOTAL_BYTES: number;
|
|
103
192
|
//# sourceMappingURL=tab-event-buffer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-event-buffer.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/tab-event-buffer.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAA;IACX,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,2CAA2C;IAC3C,IAAI,EAAE,OAAO,CAAA;IACb,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"tab-event-buffer.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/tab-event-buffer.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAA;IACX,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,2CAA2C;IAC3C,IAAI,EAAE,OAAO,CAAA;IACb,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,cAAc;IAcvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAhBhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,OAAO,CAAI;IACnB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB,CAAI;IAC7B,gEAAgE;IAChE,OAAO,CAAC,UAAU,CAAI;gBAGH,SAAS,GAAE,MAA2B,EACtC,QAAQ,GAAE,MAA2B,EACrC,GAAG,GAAE,MAAM,MAAiB,EAC5B,aAAa,GAAE,MAAgC;IAGlE;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM;IAS3C;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,EAAE;IAS3C;;;;;;;;;OASG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAKtC;;;OAGG;IACH,oBAAoB,IAAI,MAAM;IAI9B,uEAAuE;IACvE,UAAU,IAAI,MAAM;IAIpB,kDAAkD;IAClD,IAAI,IAAI,MAAM;IAId,0FAA0F;IAC1F,QAAQ,IAAI,MAAM;IAIlB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,KAAK;IAab,OAAO,CAAC,SAAS;CAOlB;AAuBD;;;;GAIG;AACH,qBAAa,sBAAsB;IAI/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAHhC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;gBAGzC,aAAa,GAAE,MAAM,cAA2C;IAGnF,8DAA8D;IAC9D,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAS1C,sDAAsD;IACtD,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAI9C,wDAAwD;IACxD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI3B,2EAA2E;IAC3E,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAA;AACxC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAiB,CAAA;AAChD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,QAAmB,CAAA"}
|
|
@@ -2,19 +2,51 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Bounded replay log for a single tab.
|
|
4
4
|
*
|
|
5
|
-
* Size/age limits are parameterised for testability but defaulted to
|
|
6
|
-
* that comfortably cover real-world reconnect windows
|
|
5
|
+
* Size/age/byte limits are parameterised for testability but defaulted to
|
|
6
|
+
* values that comfortably cover real-world reconnect windows for long-running
|
|
7
|
+
* coding-agent tasks (multi-tool, multi-minute).
|
|
8
|
+
*
|
|
9
|
+
* ## Replay-gap detection
|
|
10
|
+
*
|
|
11
|
+
* The buffer tracks `evictedThroughSeq` — the highest seq that has ever been
|
|
12
|
+
* evicted (0 if nothing has been evicted). A web client whose `lastSeenSeq`
|
|
13
|
+
* is below this value has missed events the buffer can no longer supply, and
|
|
14
|
+
* an incremental replay would produce a silent gap. Callers should consult
|
|
15
|
+
* `hasGapSince` before relying on `getSince` for incremental replay; on a
|
|
16
|
+
* gap they should fall back to a full snapshot path (e.g. `outputHistory`).
|
|
17
|
+
*
|
|
18
|
+
* ## Eviction is FIFO with three caps
|
|
19
|
+
*
|
|
20
|
+
* Events are evicted from the front when ANY of these limits is exceeded:
|
|
21
|
+
* - count: `maxEvents` (default 10k)
|
|
22
|
+
* - age: `maxAgeMs` (default 60 min)
|
|
23
|
+
* - bytes: `maxTotalBytes` (default 32 MB)
|
|
24
|
+
*
|
|
25
|
+
* The byte cap is the safety belt against pathological events (e.g. a 50 MB
|
|
26
|
+
* grep result streamed as one event). Without it, count- and age-based caps
|
|
27
|
+
* still allow a single tab to hoard arbitrary memory.
|
|
7
28
|
*/
|
|
8
29
|
export class TabEventBuffer {
|
|
9
30
|
maxEvents;
|
|
10
31
|
maxAgeMs;
|
|
11
32
|
now;
|
|
33
|
+
maxTotalBytes;
|
|
12
34
|
events = [];
|
|
13
35
|
nextSeq = 1;
|
|
14
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Highest seq that has been evicted from the buffer. 0 means nothing has
|
|
38
|
+
* been evicted yet (buffer is operating within its window). Monotonically
|
|
39
|
+
* non-decreasing — eviction always happens from the front of the FIFO, in
|
|
40
|
+
* seq order, so the most recently evicted seq is always the highest.
|
|
41
|
+
*/
|
|
42
|
+
evictedThroughSeq = 0;
|
|
43
|
+
/** Approximate sum of `byteSize` over still-resident events. */
|
|
44
|
+
totalBytes = 0;
|
|
45
|
+
constructor(maxEvents = DEFAULT_MAX_EVENTS, maxAgeMs = DEFAULT_MAX_AGE_MS, now = Date.now, maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES) {
|
|
15
46
|
this.maxEvents = maxEvents;
|
|
16
47
|
this.maxAgeMs = maxAgeMs;
|
|
17
48
|
this.now = now;
|
|
49
|
+
this.maxTotalBytes = maxTotalBytes;
|
|
18
50
|
}
|
|
19
51
|
/**
|
|
20
52
|
* Append an event and return its assigned sequence number.
|
|
@@ -25,7 +57,9 @@ export class TabEventBuffer {
|
|
|
25
57
|
*/
|
|
26
58
|
record(type, data) {
|
|
27
59
|
const seq = this.nextSeq++;
|
|
28
|
-
|
|
60
|
+
const byteSize = estimateByteSize(data);
|
|
61
|
+
this.events.push({ seq, type, data, timestamp: this.now(), byteSize });
|
|
62
|
+
this.totalBytes += byteSize;
|
|
29
63
|
this.evict();
|
|
30
64
|
return seq;
|
|
31
65
|
}
|
|
@@ -33,6 +67,11 @@ export class TabEventBuffer {
|
|
|
33
67
|
* Return all still-buffered events with `seq > afterSeq`, in original
|
|
34
68
|
* order. Returns an empty array if nothing newer is buffered (either the
|
|
35
69
|
* web is caught up or the window has rolled past).
|
|
70
|
+
*
|
|
71
|
+
* NOTE: This does not detect or signal replay gaps. Pair with
|
|
72
|
+
* `hasGapSince(afterSeq)` to know whether a returned array is a complete
|
|
73
|
+
* incremental replay or a partial one (events between `afterSeq` and the
|
|
74
|
+
* oldest surviving seq have been evicted and are no longer available).
|
|
36
75
|
*/
|
|
37
76
|
getSince(afterSeq) {
|
|
38
77
|
this.evict();
|
|
@@ -43,6 +82,27 @@ export class TabEventBuffer {
|
|
|
43
82
|
}
|
|
44
83
|
return out;
|
|
45
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* True when an incremental replay starting from `afterSeq` would silently
|
|
87
|
+
* skip events that the buffer has already evicted. Used by the replay
|
|
88
|
+
* orchestrator to decide whether to fall back to a full snapshot rather
|
|
89
|
+
* than emit a partial event stream the web can't reconstruct.
|
|
90
|
+
*
|
|
91
|
+
* `afterSeq < evictedThroughSeq` means the next event the caller expects
|
|
92
|
+
* (`afterSeq + 1`) is at or below the eviction frontier — that event has
|
|
93
|
+
* already been dropped from memory.
|
|
94
|
+
*/
|
|
95
|
+
hasGapSince(afterSeq) {
|
|
96
|
+
this.evict();
|
|
97
|
+
return afterSeq < this.evictedThroughSeq;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Highest seq that has been evicted from this buffer; 0 if nothing has been
|
|
101
|
+
* evicted yet. Exposed for telemetry and gap-recovery decisions.
|
|
102
|
+
*/
|
|
103
|
+
getEvictedThroughSeq() {
|
|
104
|
+
return this.evictedThroughSeq;
|
|
105
|
+
}
|
|
46
106
|
/** Current highest assigned seq (monotonic; not reset by eviction). */
|
|
47
107
|
currentSeq() {
|
|
48
108
|
return this.nextSeq - 1;
|
|
@@ -51,20 +111,67 @@ export class TabEventBuffer {
|
|
|
51
111
|
size() {
|
|
52
112
|
return this.events.length;
|
|
53
113
|
}
|
|
114
|
+
/** Approximate bytes held by `data` payloads currently in memory. For tests/telemetry. */
|
|
115
|
+
byteSize() {
|
|
116
|
+
return this.totalBytes;
|
|
117
|
+
}
|
|
54
118
|
/**
|
|
55
119
|
* Drop events older than `maxAgeMs` from the front, then enforce
|
|
56
|
-
* `maxEvents` by trimming the front further if needed.
|
|
57
|
-
* newest events — they're the ones the web is most
|
|
120
|
+
* `maxEvents` and `maxTotalBytes` by trimming the front further if needed.
|
|
121
|
+
* Eviction keeps the newest events — they're the ones the web is most
|
|
122
|
+
* likely to still need.
|
|
123
|
+
*
|
|
124
|
+
* Each evicted seq advances `evictedThroughSeq` so callers can detect
|
|
125
|
+
* replay gaps. The FIFO ensures we always evict in seq order, so the last
|
|
126
|
+
* evicted seq is always the highest seen so far.
|
|
127
|
+
*
|
|
128
|
+
* The byte cap is enforced LAST so that count- and age-based eviction get
|
|
129
|
+
* a chance first; a chatty-but-small session evicts on age before it ever
|
|
130
|
+
* touches the byte cap, which keeps the usual case predictable.
|
|
58
131
|
*/
|
|
59
132
|
evict() {
|
|
60
133
|
const cutoff = this.now() - this.maxAgeMs;
|
|
61
134
|
while (this.events.length > 0 && this.events[0].timestamp < cutoff) {
|
|
62
|
-
this.
|
|
135
|
+
this.popOldest();
|
|
63
136
|
}
|
|
64
137
|
while (this.events.length > this.maxEvents) {
|
|
65
|
-
this.
|
|
138
|
+
this.popOldest();
|
|
139
|
+
}
|
|
140
|
+
while (this.events.length > 0 && this.totalBytes > this.maxTotalBytes) {
|
|
141
|
+
this.popOldest();
|
|
66
142
|
}
|
|
67
143
|
}
|
|
144
|
+
popOldest() {
|
|
145
|
+
const evicted = this.events.shift();
|
|
146
|
+
if (!evicted)
|
|
147
|
+
return;
|
|
148
|
+
this.evictedThroughSeq = evicted.seq;
|
|
149
|
+
this.totalBytes -= evicted.byteSize;
|
|
150
|
+
if (this.totalBytes < 0)
|
|
151
|
+
this.totalBytes = 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Estimate `data`'s serialized byte size for the eviction byte cap. Uses
|
|
156
|
+
* `JSON.stringify` because that's what hits the wire; falls back to a small
|
|
157
|
+
* default on circular structures so we don't crash the broadcast path.
|
|
158
|
+
*
|
|
159
|
+
* `Buffer.byteLength` would give us UTF-8 bytes vs UTF-16 code units, but on
|
|
160
|
+
* Node `JSON.stringify(...).length` is close enough (within a small constant
|
|
161
|
+
* factor for ASCII-heavy payloads) and avoids an extra allocation.
|
|
162
|
+
*/
|
|
163
|
+
function estimateByteSize(data) {
|
|
164
|
+
if (data === undefined || data === null)
|
|
165
|
+
return 0;
|
|
166
|
+
try {
|
|
167
|
+
return JSON.stringify(data).length;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Circular reference, BigInt, etc. — bill a small fixed cost so the
|
|
171
|
+
// byte cap still has SOME signal. We won't be able to wire-serialize
|
|
172
|
+
// this either, but that's a separate problem.
|
|
173
|
+
return 256;
|
|
174
|
+
}
|
|
68
175
|
}
|
|
69
176
|
/**
|
|
70
177
|
* Registry of per-tab buffers. Kept as a thin collection so `HandlerContext`
|
|
@@ -99,8 +206,27 @@ export class TabEventBufferRegistry {
|
|
|
99
206
|
this.buffers.clear();
|
|
100
207
|
}
|
|
101
208
|
}
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
209
|
+
/**
|
|
210
|
+
* 10,000 events per tab.
|
|
211
|
+
*
|
|
212
|
+
* Sized for long-running coding-agent tasks (multi-tool, multi-minute) plus
|
|
213
|
+
* laptop sleep/wake reconnect windows. Worst-case observed: a 14-minute
|
|
214
|
+
* session with ~120 tool calls produces ~1.5–3k tab-scoped events; 10× that
|
|
215
|
+
* gives headroom for parallel agents and chatty improvisation. Memory
|
|
216
|
+
* footprint at ~500B/event = ~5MB per tab; the local-only single-tenant
|
|
217
|
+
* deployment makes this a non-issue.
|
|
218
|
+
*/
|
|
219
|
+
export const DEFAULT_MAX_EVENTS = 10_000;
|
|
220
|
+
/**
|
|
221
|
+
* 60 minutes of history. Covers laptop sleep/wake, long meetings between
|
|
222
|
+
* sessions, and the largest plausible reconnect window that a tab might
|
|
223
|
+
* legitimately want to recover incrementally instead of starting fresh.
|
|
224
|
+
*/
|
|
225
|
+
export const DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;
|
|
226
|
+
/**
|
|
227
|
+
* 32 MB safety belt against pathological events (large grep results, full
|
|
228
|
+
* file reads streamed inline). Eviction by bytes guarantees a single tab
|
|
229
|
+
* can't hoard arbitrary memory regardless of count/age limits.
|
|
230
|
+
*/
|
|
231
|
+
export const DEFAULT_MAX_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
106
232
|
//# sourceMappingURL=tab-event-buffer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-event-buffer.js","sourceRoot":"","sources":["../../../../server/services/websocket/tab-event-buffer.ts"],"names":[],"mappings":"AAAA,8DAA8D;
|
|
1
|
+
{"version":3,"file":"tab-event-buffer.js","sourceRoot":"","sources":["../../../../server/services/websocket/tab-event-buffer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,20 +1,36 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Replay tab-scoped events missed during a transport gap.
|
|
3
|
-
*
|
|
4
|
-
* `session-initialization.ts` calls this right before sending `tabInitialized`
|
|
5
|
-
* so the web sees any events with `seq > lastSeenSeq` in-order before the
|
|
6
|
-
* usual initialization payload. Ordering matters: e.g. a `movementComplete`
|
|
7
|
-
* before `outputHistory` would render duplicate content.
|
|
8
|
-
*
|
|
9
|
-
* Delivery is targeted (`ctx.send`) rather than broadcast because only the
|
|
10
|
-
* rejoining web needs the replay; other connected webs already saw these
|
|
11
|
-
* events live.
|
|
12
|
-
*/
|
|
13
1
|
import type { HandlerContext } from './handler-context.js';
|
|
14
2
|
import type { WSContext } from './types.js';
|
|
3
|
+
/** Result of a replay attempt — used by callers (and tests) for telemetry. */
|
|
4
|
+
export interface ReplayResult {
|
|
5
|
+
/** Number of events sent to the web during this replay. */
|
|
6
|
+
sentCount: number;
|
|
7
|
+
/**
|
|
8
|
+
* True when the buffer had already evicted events that fell between the
|
|
9
|
+
* web's `lastSeenSeq` and the oldest surviving seq. The replay is partial;
|
|
10
|
+
* the web's incremental state is now provably stale and the caller should
|
|
11
|
+
* fall back to a full snapshot path (e.g. `outputHistory`).
|
|
12
|
+
*/
|
|
13
|
+
hadGap: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* If `hadGap`, the highest seq that was evicted (so the gap range is
|
|
16
|
+
* `(lastSeenSeq + 1) .. evictedThroughSeq`). Undefined when no gap.
|
|
17
|
+
*/
|
|
18
|
+
evictedThroughSeq?: number;
|
|
19
|
+
/**
|
|
20
|
+
* If `hadGap`, the seq the web requested replay from. Echoed into
|
|
21
|
+
* telemetry so log entries are self-contained.
|
|
22
|
+
*/
|
|
23
|
+
lastSeenSeq?: number;
|
|
24
|
+
}
|
|
15
25
|
/**
|
|
16
26
|
* Replay tab events with `seq > lastSeenSeq` to `ws`. Silently no-ops when
|
|
17
27
|
* the buffer is empty or `lastSeenSeq` is unset (full init, not a resume).
|
|
28
|
+
*
|
|
29
|
+
* Returns a `ReplayResult` so the caller can detect a partial replay (the
|
|
30
|
+
* buffer evicted events the web is asking about) and decide whether to send
|
|
31
|
+
* a recovery snapshot. This is the load-bearing telemetry surface for the
|
|
32
|
+
* "long-running task output disappears mid-stream" failure mode — a `hadGap`
|
|
33
|
+
* here is the smoking gun.
|
|
18
34
|
*/
|
|
19
|
-
export declare function replayTabEventsSince(ctx: HandlerContext, ws: WSContext, tabId: string, lastSeenSeq: number | undefined):
|
|
35
|
+
export declare function replayTabEventsSince(ctx: HandlerContext, ws: WSContext, tabId: string, lastSeenSeq: number | undefined): ReplayResult;
|
|
20
36
|
//# sourceMappingURL=tab-event-replay.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-event-replay.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/tab-event-replay.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tab-event-replay.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/tab-event-replay.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,KAAK,EAAqB,SAAS,EAAE,MAAM,YAAY,CAAA;AAE9D,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,EAAE,OAAO,CAAA;IACf;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,EAAE,EAAE,SAAS,EACb,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,YAAY,CAkDd"}
|
|
@@ -1,14 +1,66 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
/**
|
|
3
|
+
* Replay tab-scoped events missed during a transport gap.
|
|
4
|
+
*
|
|
5
|
+
* `session-initialization.ts` calls this right before sending `tabInitialized`
|
|
6
|
+
* so the web sees any events with `seq > lastSeenSeq` in-order before the
|
|
7
|
+
* usual initialization payload. Ordering matters: e.g. a `movementComplete`
|
|
8
|
+
* before `outputHistory` would render duplicate content.
|
|
9
|
+
*
|
|
10
|
+
* Delivery is targeted (`ctx.send`) rather than broadcast because only the
|
|
11
|
+
* rejoining web needs the replay; other connected webs already saw these
|
|
12
|
+
* events live.
|
|
13
|
+
*/
|
|
14
|
+
import { captureException } from '../sentry.js';
|
|
2
15
|
/**
|
|
3
16
|
* Replay tab events with `seq > lastSeenSeq` to `ws`. Silently no-ops when
|
|
4
17
|
* the buffer is empty or `lastSeenSeq` is unset (full init, not a resume).
|
|
18
|
+
*
|
|
19
|
+
* Returns a `ReplayResult` so the caller can detect a partial replay (the
|
|
20
|
+
* buffer evicted events the web is asking about) and decide whether to send
|
|
21
|
+
* a recovery snapshot. This is the load-bearing telemetry surface for the
|
|
22
|
+
* "long-running task output disappears mid-stream" failure mode — a `hadGap`
|
|
23
|
+
* here is the smoking gun.
|
|
5
24
|
*/
|
|
6
25
|
export function replayTabEventsSince(ctx, ws, tabId, lastSeenSeq) {
|
|
7
26
|
if (lastSeenSeq === undefined)
|
|
8
|
-
return;
|
|
27
|
+
return { sentCount: 0, hadGap: false };
|
|
9
28
|
const buffer = ctx.tabEventBuffers.get(tabId);
|
|
10
29
|
if (!buffer)
|
|
11
|
-
return;
|
|
30
|
+
return { sentCount: 0, hadGap: false };
|
|
31
|
+
const hadGap = buffer.hasGapSince(lastSeenSeq);
|
|
32
|
+
const evictedThroughSeq = hadGap ? buffer.getEvictedThroughSeq() : undefined;
|
|
33
|
+
if (hadGap) {
|
|
34
|
+
// Replay is structurally incomplete. Surface a single, structured warning
|
|
35
|
+
// so we can grep/Sentry-search for the failure mode without spamming logs
|
|
36
|
+
// on every event.
|
|
37
|
+
const message = `[tab-replay] gap detected for tab=${tabId}: web requested replay from seq=${lastSeenSeq}, ` +
|
|
38
|
+
`but buffer has evicted through seq=${evictedThroughSeq}. ` +
|
|
39
|
+
`Events (${lastSeenSeq + 1}..${evictedThroughSeq}) are unavailable; the web's ` +
|
|
40
|
+
`incremental state is stale and a full snapshot will be sent instead.`;
|
|
41
|
+
console.warn(message);
|
|
42
|
+
try {
|
|
43
|
+
captureException(new Error('TabEventBuffer replay gap'), {
|
|
44
|
+
context: 'tab-event-replay',
|
|
45
|
+
tabId,
|
|
46
|
+
lastSeenSeq,
|
|
47
|
+
evictedThroughSeq,
|
|
48
|
+
bufferCurrentSeq: buffer.currentSeq(),
|
|
49
|
+
gapSize: (evictedThroughSeq ?? 0) - lastSeenSeq,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Sentry transport errors must not break the replay path.
|
|
54
|
+
}
|
|
55
|
+
// CRITICAL: do NOT emit partial events. If we did, the web would advance
|
|
56
|
+
// its `tabSeqs` past the (lastSeenSeq+1 .. evictedThroughSeq) range and
|
|
57
|
+
// the subsequent snapshot would land in a tab that thinks it's caught up
|
|
58
|
+
// — silently rendering only the post-gap tail. Returning early without
|
|
59
|
+
// events forces the caller (`session-initialization.ts`) into the
|
|
60
|
+
// snapshot-fallback branch, which sends a fresh `outputHistory` payload
|
|
61
|
+
// with `replayGap: true` so the web can replace its tab state cleanly.
|
|
62
|
+
return { sentCount: 0, hadGap: true, evictedThroughSeq, lastSeenSeq };
|
|
63
|
+
}
|
|
12
64
|
const events = buffer.getSince(lastSeenSeq);
|
|
13
65
|
for (const event of events) {
|
|
14
66
|
// Types are checked at record time via `broadcastTabEvent`; the buffer
|
|
@@ -16,5 +68,6 @@ export function replayTabEventsSince(ctx, ws, tabId, lastSeenSeq) {
|
|
|
16
68
|
// `WebSocketResponse['type']`. Narrow here without an extra runtime check.
|
|
17
69
|
ctx.send(ws, { type: event.type, tabId, data: event.data, seq: event.seq });
|
|
18
70
|
}
|
|
71
|
+
return { sentCount: events.length, hadGap: false, lastSeenSeq };
|
|
19
72
|
}
|
|
20
73
|
//# sourceMappingURL=tab-event-replay.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-event-replay.js","sourceRoot":"","sources":["../../../../server/services/websocket/tab-event-replay.ts"],"names":[],"mappings":"AAAA,8DAA8D;
|
|
1
|
+
{"version":3,"file":"tab-event-replay.js","sourceRoot":"","sources":["../../../../server/services/websocket/tab-event-replay.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAE9D;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AA2B/C;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAmB,EACnB,EAAa,EACb,KAAa,EACb,WAA+B;IAE/B,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IAErE,MAAM,MAAM,GAAG,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAC7C,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IAEnD,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,WAAW,CAAC,CAAA;IAC9C,MAAM,iBAAiB,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IAE5E,IAAI,MAAM,EAAE,CAAC;QACX,0EAA0E;QAC1E,0EAA0E;QAC1E,kBAAkB;QAClB,MAAM,OAAO,GACX,qCAAqC,KAAK,mCAAmC,WAAW,IAAI;YAC5F,sCAAsC,iBAAiB,IAAI;YAC3D,WAAW,WAAW,GAAG,CAAC,KAAK,iBAAiB,+BAA+B;YAC/E,sEAAsE,CAAA;QACxE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACrB,IAAI,CAAC;YACH,gBAAgB,CAAC,IAAI,KAAK,CAAC,2BAA2B,CAAC,EAAE;gBACvD,OAAO,EAAE,kBAAkB;gBAC3B,KAAK;gBACL,WAAW;gBACX,iBAAiB;gBACjB,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE;gBACrC,OAAO,EAAE,CAAC,iBAAiB,IAAI,CAAC,CAAC,GAAG,WAAW;aAChD,CAAC,CAAA;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,0DAA0D;QAC5D,CAAC;QACD,yEAAyE;QACzE,wEAAwE;QACxE,yEAAyE;QACzE,uEAAuE;QACvE,kEAAkE;QAClE,wEAAwE;QACxE,uEAAuE;QACvE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,CAAA;IACvE,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;IAC3C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,uEAAuE;QACvE,4DAA4D;QAC5D,2EAA2E;QAC3E,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAiC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAA;IAC1G,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAA;AACjE,CAAC"}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { HandlerContext } from './handler-context.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type WebSocketMessage, type WSContext } from './types.js';
|
|
3
3
|
export declare function handleGetActiveTabs(ctx: HandlerContext, ws: WSContext, workingDir: string): void;
|
|
4
4
|
export declare function handleSyncTabMeta(ctx: HandlerContext, _ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void;
|
|
5
5
|
export declare function handleRemoveTab(ctx: HandlerContext, _ws: WSContext, tabId: string, workingDir: string): void;
|
|
6
6
|
export declare function handleMarkTabViewed(ctx: HandlerContext, _ws: WSContext, tabId: string, workingDir: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Persist a per-tab engine override. `msg.data.override` is either a full
|
|
9
|
+
* `{ engine, model, effortLevel }` payload or `null` to clear the override.
|
|
10
|
+
* Persisted via the session registry so the override survives WebSocket
|
|
11
|
+
* disconnects — the core guarantee of IS-019. Broadcasts the change to all
|
|
12
|
+
* connected clients so multi-device sessions stay in sync.
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleSetTabEngine(ctx: HandlerContext, _ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void;
|
|
7
15
|
export declare function handleCreateTab(ctx: HandlerContext, ws: WSContext, workingDir: string, tabName?: string, optimisticTabId?: string): Promise<void>;
|
|
8
16
|
export declare function handleReorderTabs(ctx: HandlerContext, _ws: WSContext, workingDir: string, tabOrder?: string[]): void;
|
|
9
17
|
//# sourceMappingURL=tab-handlers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-handlers.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/tab-handlers.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"tab-handlers.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/tab-handlers.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,EAAoC,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AA2CrG,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAqBhG;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CASrI;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAY5G;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAQhH;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CA0BtI;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyDvJ;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAcpH"}
|