rivet-design 0.11.11 → 0.11.13
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/dist/install/harnesses.d.ts.map +1 -1
- package/dist/install/harnesses.js +122 -4
- package/dist/install/harnesses.js.map +1 -1
- package/dist/mcp/agent-variants/SessionStore.d.ts +150 -10
- package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
- package/dist/mcp/agent-variants/SessionStore.js +468 -23
- package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +143 -51
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js +465 -299
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +5 -4
- package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js +24 -5
- package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
- package/dist/mcp/agent-variants/contracts.d.ts +102 -89
- package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
- package/dist/mcp/agent-variants/contracts.js +29 -14
- package/dist/mcp/agent-variants/contracts.js.map +1 -1
- package/dist/mcp/agent-variants/index.d.ts +3 -3
- package/dist/mcp/agent-variants/index.d.ts.map +1 -1
- package/dist/mcp/agent-variants/index.js +1 -2
- package/dist/mcp/agent-variants/index.js.map +1 -1
- package/dist/mcp/agent-variants/previewQa.d.ts.map +1 -1
- package/dist/mcp/agent-variants/previewQa.js +0 -6
- package/dist/mcp/agent-variants/previewQa.js.map +1 -1
- package/dist/mcp/agent-variants/tools.d.ts +12 -4
- package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
- package/dist/mcp/agent-variants/tools.js +69 -13
- package/dist/mcp/agent-variants/tools.js.map +1 -1
- package/dist/mcp/changeBatchClassification.d.ts +30 -0
- package/dist/mcp/changeBatchClassification.d.ts.map +1 -1
- package/dist/mcp/changeBatchClassification.js +76 -0
- package/dist/mcp/changeBatchClassification.js.map +1 -1
- package/dist/mcp/instructions.d.ts.map +1 -1
- package/dist/mcp/instructions.js +7 -1
- package/dist/mcp/instructions.js.map +1 -1
- package/dist/mcp/server.d.ts +51 -3
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +404 -156
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/watchLoopResumePrompt.d.ts +16 -0
- package/dist/mcp/watchLoopResumePrompt.d.ts.map +1 -0
- package/dist/mcp/watchLoopResumePrompt.js +21 -0
- package/dist/mcp/watchLoopResumePrompt.js.map +1 -0
- package/dist/routes/agentVariants.d.ts +10 -1
- package/dist/routes/agentVariants.d.ts.map +1 -1
- package/dist/routes/agentVariants.js +214 -3
- package/dist/routes/agentVariants.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +106 -47
- package/dist/routes/mcp.js.map +1 -1
- package/dist/services/BridgeBatchPipeline.d.ts +99 -0
- package/dist/services/BridgeBatchPipeline.d.ts.map +1 -0
- package/dist/services/BridgeBatchPipeline.js +278 -0
- package/dist/services/BridgeBatchPipeline.js.map +1 -0
- package/dist/services/BridgeQueueStore.d.ts +28 -0
- package/dist/services/BridgeQueueStore.d.ts.map +1 -0
- package/dist/services/BridgeQueueStore.js +88 -0
- package/dist/services/BridgeQueueStore.js.map +1 -0
- package/dist/services/InlineVariantGenerationService.d.ts.map +1 -1
- package/dist/services/InlineVariantGenerationService.js +15 -3
- package/dist/services/InlineVariantGenerationService.js.map +1 -1
- package/dist/services/SessionBridgeService.d.ts +127 -31
- package/dist/services/SessionBridgeService.d.ts.map +1 -1
- package/dist/services/SessionBridgeService.js +428 -83
- package/dist/services/SessionBridgeService.js.map +1 -1
- package/dist/services/TelemetryService.d.ts +30 -0
- package/dist/services/TelemetryService.d.ts.map +1 -1
- package/dist/services/TelemetryService.js +42 -0
- package/dist/services/TelemetryService.js.map +1 -1
- package/dist/services/WorktreeManager.d.ts +29 -2
- package/dist/services/WorktreeManager.d.ts.map +1 -1
- package/dist/services/WorktreeManager.js +73 -7
- package/dist/services/WorktreeManager.js.map +1 -1
- package/dist/services/createAgentVariantsOrchestrator.d.ts +0 -8
- package/dist/services/createAgentVariantsOrchestrator.d.ts.map +1 -1
- package/dist/services/createAgentVariantsOrchestrator.js +1 -18
- package/dist/services/createAgentVariantsOrchestrator.js.map +1 -1
- package/dist/utils/portUtils.d.ts +9 -0
- package/dist/utils/portUtils.d.ts.map +1 -1
- package/dist/utils/portUtils.js +26 -0
- package/dist/utils/portUtils.js.map +1 -1
- package/dist/utils/skills/claude-skill.d.ts +1 -1
- package/dist/utils/skills/claude-skill.d.ts.map +1 -1
- package/dist/utils/skills/claude-skill.js +31 -12
- package/dist/utils/skills/claude-skill.js.map +1 -1
- package/dist/utils/skills/cursor-rules.d.ts +1 -1
- package/dist/utils/skills/cursor-rules.d.ts.map +1 -1
- package/dist/utils/skills/cursor-rules.js +26 -11
- package/dist/utils/skills/cursor-rules.js.map +1 -1
- package/dist/utils/skills/shared-variants-protocol.d.ts +28 -0
- package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
- package/dist/utils/skills/shared-variants-protocol.js +88 -14
- package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
- package/package.json +3 -2
- package/src/ui/dist/assets/main-DREt3GGU.css +1 -0
- package/src/ui/dist/assets/main-Dt7ShmjM.js +655 -0
- package/src/ui/dist/index.html +2 -2
- package/dist/mcp/agent-variants/designCritique.d.ts +0 -167
- package/dist/mcp/agent-variants/designCritique.d.ts.map +0 -1
- package/dist/mcp/agent-variants/designCritique.js +0 -774
- package/dist/mcp/agent-variants/designCritique.js.map +0 -1
- package/src/ui/dist/assets/main-B1LAuLK_.js +0 -655
- package/src/ui/dist/assets/main-D-YwdYjm.css +0 -1
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SessionBridgeService = void 0;
|
|
3
|
+
exports.SessionBridgeService = exports.MAX_APPLY_BATCH_BYTES = exports.MAX_VARIANT_REQUEST_BYTES = exports.MAX_LEASED_VARIANT_BATCHES = exports.MAX_PENDING_APPLY_BATCHES = exports.MAX_PENDING_VARIANT_REQUESTS = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const index_core_1 = require("../index-core");
|
|
6
|
+
const changeBatchClassification_1 = require("../mcp/changeBatchClassification");
|
|
7
|
+
const BridgeBatchPipeline_1 = require("./BridgeBatchPipeline");
|
|
8
|
+
const BridgeQueueStore_1 = require("./BridgeQueueStore");
|
|
6
9
|
const log = (0, index_core_1.createLogger)('SessionBridgeService');
|
|
10
|
+
/**
|
|
11
|
+
* Upper bound on the variant-request FIFO depth. The queue intentionally
|
|
12
|
+
* appends (never clobbers) so concurrent "Vary" runs aren't dropped, but
|
|
13
|
+
* without a cap a caller hitting the unauthenticated queue endpoint could
|
|
14
|
+
* append unboundedly and exhaust server memory. 16 is far above any real
|
|
15
|
+
* concurrent-Vary workload; past it we reject with backpressure (HTTP 429)
|
|
16
|
+
* so the request surfaces as an error the user can retry, rather than being
|
|
17
|
+
* silently dropped or growing the queue.
|
|
18
|
+
*/
|
|
19
|
+
exports.MAX_PENDING_VARIANT_REQUESTS = 16;
|
|
20
|
+
/** FIFO depth cap for apply batches (was a single overwrite slot). */
|
|
21
|
+
exports.MAX_PENDING_APPLY_BATCHES = 16;
|
|
22
|
+
/** Maximum concurrent variant batches leased to agents/watchers. */
|
|
23
|
+
exports.MAX_LEASED_VARIANT_BATCHES = 4;
|
|
24
|
+
/**
|
|
25
|
+
* Per-batch byte budget for a queued variant-request/refine batch. The count
|
|
26
|
+
* cap above bounds HOW MANY batches queue; this bounds how BIG one can be. The
|
|
27
|
+
* route schema already caps attached images, but `instruction` / `sourceFiles`
|
|
28
|
+
* / element metadata are otherwise bounded only by the (much larger) HTTP body
|
|
29
|
+
* limit — so without this a local/LAN caller could pin memory with a few large
|
|
30
|
+
* batches × the FIFO depth. ~2MB of non-image text/metadata is generous.
|
|
31
|
+
*/
|
|
32
|
+
exports.MAX_VARIANT_REQUEST_BYTES = 2 * 1024 * 1024;
|
|
33
|
+
/** Same per-batch byte cap for apply batches queued on the apply FIFO. */
|
|
34
|
+
exports.MAX_APPLY_BATCH_BYTES = exports.MAX_VARIANT_REQUEST_BYTES;
|
|
7
35
|
/**
|
|
8
36
|
* Manages the lifecycle of an MCP visual editing session and queues
|
|
9
37
|
* changes between the Rivet UI and the MCP server.
|
|
@@ -18,12 +46,35 @@ class SessionBridgeService {
|
|
|
18
46
|
_isVariantPortAllowed = null;
|
|
19
47
|
statusAccessToken = null;
|
|
20
48
|
projectPath = null;
|
|
21
|
-
|
|
49
|
+
applyPipeline = new BridgeBatchPipeline_1.BridgeBatchPipeline({
|
|
50
|
+
kind: 'apply',
|
|
51
|
+
maxQueued: exports.MAX_PENDING_APPLY_BATCHES,
|
|
52
|
+
maxConcurrentLeases: 1,
|
|
53
|
+
queueFullReason: 'apply_queue_full',
|
|
54
|
+
});
|
|
55
|
+
variantPipeline = new BridgeBatchPipeline_1.BridgeBatchPipeline({
|
|
56
|
+
kind: 'variant',
|
|
57
|
+
maxQueued: exports.MAX_PENDING_VARIANT_REQUESTS,
|
|
58
|
+
maxConcurrentLeases: exports.MAX_LEASED_VARIANT_BATCHES,
|
|
59
|
+
queueFullReason: 'variant_queue_full',
|
|
60
|
+
});
|
|
22
61
|
telemetryContext = null;
|
|
23
62
|
roundsCompleted = 0;
|
|
24
63
|
feedback = [];
|
|
25
64
|
status = 'IDLE';
|
|
26
|
-
|
|
65
|
+
// Blocking watch_for_changes waiters, FIFO. A multi-element queue (not a
|
|
66
|
+
// single slot) so several concurrent watchers can each block and each wake on
|
|
67
|
+
// a DIFFERENT queued change — one change wakes one waiter — letting parallel
|
|
68
|
+
// agents pick up distinct batches instead of clobbering each other's waiter.
|
|
69
|
+
waitResolvers = [];
|
|
70
|
+
/**
|
|
71
|
+
* Subscribers notified whenever {@link WatcherLiveness} could have changed
|
|
72
|
+
* (queue/drain/waiter register/settle). The variants SSE stream subscribes so
|
|
73
|
+
* a strand the user causes — sending a change with no watcher draining it —
|
|
74
|
+
* re-pushes the snapshot promptly instead of waiting on the next orchestrator
|
|
75
|
+
* event, which a stranded queue never produces.
|
|
76
|
+
*/
|
|
77
|
+
livenessListeners = new Set();
|
|
27
78
|
activeRunCorrelation = null;
|
|
28
79
|
baseDevServerHealth = null;
|
|
29
80
|
devServerHealth = {
|
|
@@ -46,9 +97,28 @@ class SessionBridgeService {
|
|
|
46
97
|
}
|
|
47
98
|
this.sessionId = (0, crypto_1.randomUUID)();
|
|
48
99
|
this.projectPath = projectPath;
|
|
49
|
-
this.
|
|
100
|
+
this.applyPipeline = new BridgeBatchPipeline_1.BridgeBatchPipeline({
|
|
101
|
+
kind: 'apply',
|
|
102
|
+
maxQueued: exports.MAX_PENDING_APPLY_BATCHES,
|
|
103
|
+
maxConcurrentLeases: 1,
|
|
104
|
+
queueFullReason: 'apply_queue_full',
|
|
105
|
+
});
|
|
106
|
+
this.variantPipeline = new BridgeBatchPipeline_1.BridgeBatchPipeline({
|
|
107
|
+
kind: 'variant',
|
|
108
|
+
maxQueued: exports.MAX_PENDING_VARIANT_REQUESTS,
|
|
109
|
+
maxConcurrentLeases: exports.MAX_LEASED_VARIANT_BATCHES,
|
|
110
|
+
queueFullReason: 'variant_queue_full',
|
|
111
|
+
});
|
|
112
|
+
const persisted = BridgeQueueStore_1.bridgeQueueStore.load(projectPath, this.sessionId);
|
|
113
|
+
if (persisted) {
|
|
114
|
+
this.applyPipeline.hydrate(persisted.apply);
|
|
115
|
+
this.variantPipeline.hydrate(persisted.variant);
|
|
116
|
+
this.roundsCompleted = persisted.roundsCompleted;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.roundsCompleted = 0;
|
|
120
|
+
}
|
|
50
121
|
this.telemetryContext = null;
|
|
51
|
-
this.roundsCompleted = 0;
|
|
52
122
|
this.feedback = [];
|
|
53
123
|
this.status = 'IDLE';
|
|
54
124
|
this.activeRunCorrelation = null;
|
|
@@ -66,8 +136,32 @@ class SessionBridgeService {
|
|
|
66
136
|
startedByRivet: false,
|
|
67
137
|
};
|
|
68
138
|
log.info(`Session started: ${this.sessionId} for ${projectPath}`);
|
|
139
|
+
this.persistQueue();
|
|
140
|
+
this.notifyLivenessChange();
|
|
69
141
|
return this.sessionId;
|
|
70
142
|
}
|
|
143
|
+
/** Write the current queue snapshot to disk for MCP restart recovery. */
|
|
144
|
+
persistQueue() {
|
|
145
|
+
if (!this.sessionId || !this.projectPath) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
BridgeQueueStore_1.bridgeQueueStore.save(this.projectPath, this.sessionId, {
|
|
149
|
+
...(0, BridgeQueueStore_1.emptyBridgeQueueSnapshot)(this.projectPath),
|
|
150
|
+
apply: this.applyPipeline.snapshot(),
|
|
151
|
+
variant: this.variantPipeline.snapshot(),
|
|
152
|
+
roundsCompleted: this.roundsCompleted,
|
|
153
|
+
updatedAt: Date.now(),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
buildQueuedBatch(intent) {
|
|
157
|
+
const requestId = intent.agentRunId ?? intent.traceId ?? (0, crypto_1.randomUUID)();
|
|
158
|
+
return {
|
|
159
|
+
intent,
|
|
160
|
+
enqueuedAt: Date.now(),
|
|
161
|
+
leaseId: (0, crypto_1.randomUUID)(),
|
|
162
|
+
requestId,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
71
165
|
/**
|
|
72
166
|
* Add a feedback message to the session.
|
|
73
167
|
* Feedback is shown in the Rivet UI to let the user know what the MCP client is doing.
|
|
@@ -127,30 +221,162 @@ class SessionBridgeService {
|
|
|
127
221
|
}
|
|
128
222
|
/**
|
|
129
223
|
* Queue changes from the Rivet UI for retrieval by the MCP client.
|
|
130
|
-
*
|
|
224
|
+
* Each send appends to the apply or variant FIFO (never overwrites).
|
|
131
225
|
*/
|
|
132
226
|
queueChangeRequest(intent) {
|
|
133
227
|
if (!this.sessionId) {
|
|
134
228
|
log.warn('Cannot queue changes: no active session');
|
|
135
|
-
return;
|
|
229
|
+
return { accepted: true };
|
|
136
230
|
}
|
|
137
|
-
// Clear feedback from previous round when a new round starts
|
|
138
231
|
this.feedback = [];
|
|
139
|
-
this.pendingChanges = intent;
|
|
140
232
|
this.updateTelemetryContext(intent.telemetryContext);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
233
|
+
let enqueueResult;
|
|
234
|
+
if (!(0, changeBatchClassification_1.isMixedVariantBatch)(intent) &&
|
|
235
|
+
((0, changeBatchClassification_1.isSingleVariantRequestBatch)(intent) || (0, changeBatchClassification_1.isStaticPreviewRefineBatch)(intent))) {
|
|
236
|
+
const approxBytes = Buffer.byteLength(JSON.stringify(intent));
|
|
237
|
+
if (approxBytes > exports.MAX_VARIANT_REQUEST_BYTES) {
|
|
238
|
+
log.warn(`Variant-request batch too large (${approxBytes} bytes > ${exports.MAX_VARIANT_REQUEST_BYTES}); rejecting`);
|
|
239
|
+
return { accepted: false, reason: 'variant_request_too_large' };
|
|
240
|
+
}
|
|
241
|
+
enqueueResult = this.variantPipeline.enqueue(this.buildQueuedBatch(intent));
|
|
242
|
+
if (enqueueResult.accepted) {
|
|
243
|
+
log.info(`Variant-request batch queued (${this.variantPipeline.queuedDepth()} pending): ${intent.changes.length} changes`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
log.warn(`Variant-request queue full (${exports.MAX_PENDING_VARIANT_REQUESTS} pending); rejecting new batch`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const approxBytes = Buffer.byteLength(JSON.stringify(intent));
|
|
251
|
+
if (approxBytes > exports.MAX_APPLY_BATCH_BYTES) {
|
|
252
|
+
log.warn(`Apply batch too large (${approxBytes} bytes > ${exports.MAX_APPLY_BATCH_BYTES}); rejecting`);
|
|
253
|
+
return { accepted: false, reason: 'apply_batch_too_large' };
|
|
254
|
+
}
|
|
255
|
+
enqueueResult = this.applyPipeline.enqueue(this.buildQueuedBatch(intent));
|
|
256
|
+
if (enqueueResult.accepted) {
|
|
257
|
+
log.info(`Change request queued (Round ${this.roundsCompleted + 1}): ${intent.changes.length} changes, ${intent.sourceFiles.length} source files`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
log.warn(`Apply queue full (${exports.MAX_PENDING_APPLY_BATCHES} pending); rejecting new batch`);
|
|
261
|
+
}
|
|
146
262
|
}
|
|
263
|
+
if (!enqueueResult.accepted) {
|
|
264
|
+
return enqueueResult;
|
|
265
|
+
}
|
|
266
|
+
this.persistQueue();
|
|
267
|
+
const waiter = this.waitResolvers.shift();
|
|
268
|
+
if (waiter)
|
|
269
|
+
waiter(intent);
|
|
270
|
+
this.notifyLivenessChange();
|
|
271
|
+
return { accepted: true };
|
|
147
272
|
}
|
|
148
273
|
/**
|
|
149
|
-
* Retrieve
|
|
150
|
-
* Does NOT clear the queue — call clearPending() after processing.
|
|
274
|
+
* Retrieve the oldest queued apply batch intent without leasing it.
|
|
151
275
|
*/
|
|
152
276
|
getPendingChanges() {
|
|
153
|
-
return this.
|
|
277
|
+
return this.applyPipeline.peekHeadIntent();
|
|
278
|
+
}
|
|
279
|
+
/** True when one or more variant-request/refine batches are queued or leased. */
|
|
280
|
+
hasPendingVariantRequests() {
|
|
281
|
+
return this.variantPipeline.hasWork();
|
|
282
|
+
}
|
|
283
|
+
/** Queued variant batches still waiting for lease pickup. */
|
|
284
|
+
pendingVariantRequestCount() {
|
|
285
|
+
return this.variantPipeline.queuedDepth();
|
|
286
|
+
}
|
|
287
|
+
/** Non-destructive snapshot of queued variant-request/refine batches. */
|
|
288
|
+
peekVariantRequests() {
|
|
289
|
+
return this.variantPipeline.peekQueuedIntents();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Lease the oldest eligible batch across apply and variant channels.
|
|
293
|
+
* Global FIFO uses oldest `enqueuedAt` among channels that can lease.
|
|
294
|
+
*/
|
|
295
|
+
leaseBatch(requestId) {
|
|
296
|
+
if (requestId) {
|
|
297
|
+
const applyLease = this.applyPipeline.leaseNext(requestId);
|
|
298
|
+
if (applyLease) {
|
|
299
|
+
this.persistQueue();
|
|
300
|
+
this.notifyLivenessChange();
|
|
301
|
+
return applyLease;
|
|
302
|
+
}
|
|
303
|
+
const variantLease = this.variantPipeline.leaseNext(requestId);
|
|
304
|
+
if (variantLease) {
|
|
305
|
+
this.persistQueue();
|
|
306
|
+
this.notifyLivenessChange();
|
|
307
|
+
return variantLease;
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const candidates = [];
|
|
312
|
+
const applyHeadAt = this.applyPipeline.oldestQueuedEnqueuedAt();
|
|
313
|
+
if (this.applyPipeline.canLease() && applyHeadAt !== null) {
|
|
314
|
+
candidates.push({ kind: 'apply', enqueuedAt: applyHeadAt });
|
|
315
|
+
}
|
|
316
|
+
const variantHeadAt = this.variantPipeline.oldestQueuedEnqueuedAt();
|
|
317
|
+
if (this.variantPipeline.canLease() && variantHeadAt !== null) {
|
|
318
|
+
candidates.push({ kind: 'variant', enqueuedAt: variantHeadAt });
|
|
319
|
+
}
|
|
320
|
+
if (candidates.length === 0) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
candidates.sort((a, b) => a.enqueuedAt - b.enqueuedAt);
|
|
324
|
+
const head = candidates[0];
|
|
325
|
+
const lease = head.kind === 'apply'
|
|
326
|
+
? this.applyPipeline.leaseNext()
|
|
327
|
+
: this.variantPipeline.leaseNext();
|
|
328
|
+
if (lease) {
|
|
329
|
+
this.persistQueue();
|
|
330
|
+
this.notifyLivenessChange();
|
|
331
|
+
}
|
|
332
|
+
return lease;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Release a leased batch after the agent completes work (`refresh_git` or `ack_batch`).
|
|
336
|
+
*/
|
|
337
|
+
ackBatch(params) {
|
|
338
|
+
if (this.applyPipeline.findLease(params.leaseId)) {
|
|
339
|
+
const hadLease = Boolean(this.applyPipeline.findLease(params.leaseId));
|
|
340
|
+
const result = this.applyPipeline.ack(params);
|
|
341
|
+
if (hadLease) {
|
|
342
|
+
this.roundsCompleted += 1;
|
|
343
|
+
}
|
|
344
|
+
this.persistQueue();
|
|
345
|
+
this.notifyLivenessChange();
|
|
346
|
+
const applyAck = { ...result, kind: 'apply' };
|
|
347
|
+
return applyAck;
|
|
348
|
+
}
|
|
349
|
+
if (this.variantPipeline.findLease(params.leaseId)) {
|
|
350
|
+
const result = this.variantPipeline.ack(params);
|
|
351
|
+
this.persistQueue();
|
|
352
|
+
this.notifyLivenessChange();
|
|
353
|
+
const variantAck = { ...result, kind: 'variant' };
|
|
354
|
+
return variantAck;
|
|
355
|
+
}
|
|
356
|
+
return { acked: true, mergedRequestIds: [] };
|
|
357
|
+
}
|
|
358
|
+
/** True when either channel has queued or in-flight leased work. */
|
|
359
|
+
hasUnfinishedWork() {
|
|
360
|
+
return this.applyPipeline.hasWork() || this.variantPipeline.hasWork();
|
|
361
|
+
}
|
|
362
|
+
/** Queue and leased depth for one channel (status API). */
|
|
363
|
+
getChannelDepth(kind) {
|
|
364
|
+
const pipeline = kind === 'apply' ? this.applyPipeline : this.variantPipeline;
|
|
365
|
+
return {
|
|
366
|
+
queued: pipeline.queuedDepth(),
|
|
367
|
+
leased: pipeline.leasedDepth(),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/** @deprecated Use {@link ackBatch} after the agent completes a leased apply batch. */
|
|
371
|
+
shiftVariantRequest() {
|
|
372
|
+
const lease = this.variantPipeline.leaseNext();
|
|
373
|
+
if (!lease) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
this.variantPipeline.ack({ leaseId: lease.leaseId, requestId: lease.requestId });
|
|
377
|
+
this.persistQueue();
|
|
378
|
+
this.notifyLivenessChange();
|
|
379
|
+
return lease.intent;
|
|
154
380
|
}
|
|
155
381
|
/**
|
|
156
382
|
* Block until changes are queued, or return immediately if already pending.
|
|
@@ -158,89 +384,115 @@ class SessionBridgeService {
|
|
|
158
384
|
* Rejects if the timeout is reached.
|
|
159
385
|
*/
|
|
160
386
|
waitForChanges(timeoutMs = 300_000) {
|
|
161
|
-
|
|
162
|
-
|
|
387
|
+
const existing = this.applyPipeline.peekHeadIntent() ??
|
|
388
|
+
this.variantPipeline.peekHeadIntent();
|
|
389
|
+
if (existing) {
|
|
390
|
+
return Promise.resolve(existing);
|
|
163
391
|
}
|
|
164
392
|
return new Promise((resolve, reject) => {
|
|
165
|
-
const
|
|
166
|
-
this.waitResolve = null;
|
|
167
|
-
reject(new Error('Timed out waiting for design changes'));
|
|
168
|
-
}, timeoutMs);
|
|
169
|
-
this.waitResolve = (intent) => {
|
|
393
|
+
const resolver = (intent) => {
|
|
170
394
|
clearTimeout(timer);
|
|
171
395
|
resolve(intent);
|
|
172
396
|
};
|
|
397
|
+
const timer = setTimeout(() => {
|
|
398
|
+
const idx = this.waitResolvers.indexOf(resolver);
|
|
399
|
+
if (idx !== -1)
|
|
400
|
+
this.waitResolvers.splice(idx, 1);
|
|
401
|
+
this.notifyLivenessChange();
|
|
402
|
+
reject(new Error('Timed out waiting for design changes'));
|
|
403
|
+
}, timeoutMs);
|
|
404
|
+
this.waitResolvers.push(resolver);
|
|
405
|
+
this.notifyLivenessChange();
|
|
173
406
|
});
|
|
174
407
|
}
|
|
175
408
|
/**
|
|
176
|
-
*
|
|
177
|
-
* Call after the MCP client has retrieved and processed the intent.
|
|
409
|
+
* Ack the active apply lease after processing (legacy alias for tests).
|
|
178
410
|
*/
|
|
179
411
|
clearPending(expectedIntent) {
|
|
180
|
-
|
|
412
|
+
const snapshot = this.applyPipeline.snapshot();
|
|
413
|
+
const activeLease = snapshot.leased[0];
|
|
414
|
+
if (activeLease) {
|
|
415
|
+
if (expectedIntent && activeLease.intent !== expectedIntent) {
|
|
416
|
+
log.info('Pending intent changed while processing; preserving newer intent');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
this.ackBatch({
|
|
420
|
+
leaseId: activeLease.leaseId,
|
|
421
|
+
requestId: activeLease.requestId,
|
|
422
|
+
});
|
|
423
|
+
log.info(`Pending intent cleared, round ${this.roundsCompleted} complete`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const head = this.applyPipeline.peekHeadIntent();
|
|
427
|
+
if (!head) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (expectedIntent && head !== expectedIntent) {
|
|
181
431
|
log.info('Pending intent changed while processing; preserving newer intent');
|
|
182
432
|
return;
|
|
183
433
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.
|
|
434
|
+
const lease = this.applyPipeline.leaseNext();
|
|
435
|
+
if (lease) {
|
|
436
|
+
this.ackBatch({ leaseId: lease.leaseId, requestId: lease.requestId });
|
|
187
437
|
log.info(`Pending intent cleared, round ${this.roundsCompleted} complete`);
|
|
188
438
|
}
|
|
189
439
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Consume a single-item variant-request batch from the pending queue.
|
|
192
|
-
*
|
|
193
|
-
* Variant-request batches are not "applied" — they are routed into the MCP
|
|
194
|
-
* variants lifecycle (propose_variants → approval → continue → commit). They
|
|
195
|
-
* must NOT call startApplyingRun, must NOT fire trackChangesQueued /
|
|
196
|
-
* trackAgentRunStarted, and must NOT increment the apply-run round counter.
|
|
197
|
-
*
|
|
198
|
-
* Returns the consumed intent so callers can derive the agent's
|
|
199
|
-
* propose_variants target from it.
|
|
200
|
-
*/
|
|
440
|
+
/** @deprecated Leased batches are released via {@link ackBatch}. */
|
|
201
441
|
consumeVariantRequestBatch(expectedIntent) {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
log.info('Pending intent changed while consuming variant request; preserving newer intent');
|
|
442
|
+
const lease = expectedIntent
|
|
443
|
+
? this.findVariantLeaseForIntent(expectedIntent)
|
|
444
|
+
: this.variantPipeline.leaseNext();
|
|
445
|
+
if (!lease) {
|
|
207
446
|
return null;
|
|
208
447
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// mirroring the queue/wake symmetry of queueChangeRequest. (Defensive:
|
|
212
|
-
// the consume path is normally reached from a watch_for_changes caller
|
|
213
|
-
// that has already resolved; this is a no-op in that case.)
|
|
214
|
-
this.waitResolve = null;
|
|
215
|
-
log.info(`Variant-request batch consumed (no apply run started, ${intent.changes.length} items)`);
|
|
216
|
-
return intent;
|
|
448
|
+
log.info(`variant-request batch consumed (no apply run started, ${lease.intent.changes.length} items)`);
|
|
449
|
+
return lease.intent;
|
|
217
450
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Consume a static-preview refinement batch from the pending queue.
|
|
220
|
-
*
|
|
221
|
-
* These batches refine an in-memory HTML variant through the variants MCP
|
|
222
|
-
* lifecycle. They must not start a base project apply run or increment the
|
|
223
|
-
* apply-round counter.
|
|
224
|
-
*/
|
|
451
|
+
/** @deprecated Leased batches are released via {@link ackBatch}. */
|
|
225
452
|
consumeStaticPreviewRefineBatch(expectedIntent) {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
log.info('Pending intent changed while consuming static-preview refine request; preserving newer intent');
|
|
453
|
+
const lease = expectedIntent
|
|
454
|
+
? this.findVariantLeaseForIntent(expectedIntent)
|
|
455
|
+
: this.variantPipeline.leaseNext();
|
|
456
|
+
if (!lease) {
|
|
231
457
|
return null;
|
|
232
458
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
459
|
+
log.info(`static-preview refine batch consumed (no apply run started, ${lease.intent.changes.length} items)`);
|
|
460
|
+
return lease.intent;
|
|
461
|
+
}
|
|
462
|
+
findVariantLeaseForIntent(expectedIntent) {
|
|
463
|
+
const requestId = expectedIntent.agentRunId ?? expectedIntent.traceId;
|
|
464
|
+
if (requestId) {
|
|
465
|
+
const existing = this.variantPipeline.leaseNext(requestId);
|
|
466
|
+
if (existing) {
|
|
467
|
+
return existing;
|
|
468
|
+
}
|
|
469
|
+
const leased = this.variantPipeline.leaseQueuedRequestId(requestId);
|
|
470
|
+
if (leased) {
|
|
471
|
+
this.persistQueue();
|
|
472
|
+
this.notifyLivenessChange();
|
|
473
|
+
return leased;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
const head = this.variantPipeline.peekHeadIntent();
|
|
478
|
+
if (head === expectedIntent) {
|
|
479
|
+
const leased = this.variantPipeline.leaseNext();
|
|
480
|
+
if (leased) {
|
|
481
|
+
this.persistQueue();
|
|
482
|
+
this.notifyLivenessChange();
|
|
483
|
+
return leased;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
log.info('Pending variant batch no longer queued (already consumed/superseded)');
|
|
488
|
+
return null;
|
|
237
489
|
}
|
|
238
490
|
/**
|
|
239
491
|
* Remove variant-request comment items from a mixed pending batch while
|
|
240
492
|
* preserving all non-variant items for a follow-up apply run.
|
|
241
493
|
*/
|
|
242
494
|
rejectMixedVariantRequestBatch(expectedIntent) {
|
|
243
|
-
const intent = this.
|
|
495
|
+
const intent = this.applyPipeline.peekHeadIntent();
|
|
244
496
|
if (!intent)
|
|
245
497
|
return null;
|
|
246
498
|
if (expectedIntent && intent !== expectedIntent) {
|
|
@@ -253,17 +505,21 @@ class SessionBridgeService {
|
|
|
253
505
|
return intent;
|
|
254
506
|
}
|
|
255
507
|
if (nextChanges.length === 0) {
|
|
256
|
-
this.
|
|
257
|
-
this.waitResolve = null;
|
|
508
|
+
this.applyPipeline.replaceHeadQueuedBatch(null);
|
|
258
509
|
log.info('Mixed variant batch rejection removed all pending changes');
|
|
510
|
+
this.persistQueue();
|
|
511
|
+
this.notifyLivenessChange();
|
|
259
512
|
return null;
|
|
260
513
|
}
|
|
261
|
-
|
|
514
|
+
const preserved = {
|
|
262
515
|
...intent,
|
|
263
516
|
changes: nextChanges,
|
|
264
517
|
};
|
|
518
|
+
this.applyPipeline.replaceHeadQueuedBatch(preserved);
|
|
265
519
|
log.info(`Mixed variant batch rejected; preserved ${nextChanges.length} non-variant items`);
|
|
266
|
-
|
|
520
|
+
this.persistQueue();
|
|
521
|
+
this.notifyLivenessChange();
|
|
522
|
+
return preserved;
|
|
267
523
|
}
|
|
268
524
|
/**
|
|
269
525
|
* Mark the currently processing MCP apply run using identifiers from the queued request.
|
|
@@ -291,6 +547,11 @@ class SessionBridgeService {
|
|
|
291
547
|
this.activeRunCorrelation = null;
|
|
292
548
|
return correlation;
|
|
293
549
|
}
|
|
550
|
+
/** Active apply lease, if any (at most one). */
|
|
551
|
+
findActiveApplyLease() {
|
|
552
|
+
const leased = this.applyPipeline.snapshot().leased;
|
|
553
|
+
return leased[0] ?? null;
|
|
554
|
+
}
|
|
294
555
|
/** Get the number of completed editing rounds in this session */
|
|
295
556
|
getRoundsCompleted() {
|
|
296
557
|
return this.roundsCompleted;
|
|
@@ -385,12 +646,11 @@ class SessionBridgeService {
|
|
|
385
646
|
* preview) on `devServerHealth` so `/api/mcp/status` and the iframe's
|
|
386
647
|
* `usePreviewUpstreamHealth` probe see `ownership: 'rivet_owned'` + the
|
|
387
648
|
* new port. Pass `null` to clear back to the no-server state. Distinct
|
|
388
|
-
* from `switchPreviewPort` (which
|
|
649
|
+
* from `switchPreviewPort` (which temporarily retargets health while a
|
|
650
|
+
* variant preview is active, then restores `baseDevServerHealth`) and from
|
|
389
651
|
* `setDevServerContext` (which the MCP server uses for the user's own
|
|
390
652
|
* dev server during a regular session). Only the orchestrator's
|
|
391
|
-
* commit-handoff path should call this
|
|
392
|
-
* still go through `switchPreviewPort` and must NOT touch the user's
|
|
393
|
-
* external dev-server health.
|
|
653
|
+
* commit-handoff path should call this.
|
|
394
654
|
*/
|
|
395
655
|
setCommittedDevServerHealth(port) {
|
|
396
656
|
if (port === null) {
|
|
@@ -412,7 +672,7 @@ class SessionBridgeService {
|
|
|
412
672
|
const committedDevServerHealth = {
|
|
413
673
|
...this.devServerHealth,
|
|
414
674
|
ownership: 'rivet_owned',
|
|
415
|
-
host: '
|
|
675
|
+
host: 'localhost',
|
|
416
676
|
port,
|
|
417
677
|
isReachable: true,
|
|
418
678
|
reason: 'healthy',
|
|
@@ -431,6 +691,24 @@ class SessionBridgeService {
|
|
|
431
691
|
log.info(`switchPreviewPort → ${port === null ? 'default upstream' : `localhost:${port}`}`);
|
|
432
692
|
}
|
|
433
693
|
this._setVariantTarget?.(port);
|
|
694
|
+
if (port !== null) {
|
|
695
|
+
// Retarget `/api/mcp/status` upstream probes to the active variant dev
|
|
696
|
+
// server. The proxy already serves this port; without this, health keeps
|
|
697
|
+
// checking the (possibly dead) base external port and the iframe shows
|
|
698
|
+
// "Preview isn't connected" while variant previews are live.
|
|
699
|
+
this.devServerHealth = {
|
|
700
|
+
ownership: 'rivet_owned',
|
|
701
|
+
host: 'localhost',
|
|
702
|
+
port,
|
|
703
|
+
isReachable: true,
|
|
704
|
+
reason: 'healthy',
|
|
705
|
+
lastCheckedAt: Date.now(),
|
|
706
|
+
startedByRivet: true,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
else if (this.baseDevServerHealth) {
|
|
710
|
+
this.devServerHealth = { ...this.baseDevServerHealth };
|
|
711
|
+
}
|
|
434
712
|
this.reportFeedback('__RIVET_RELOAD_PREVIEW__');
|
|
435
713
|
}
|
|
436
714
|
/**
|
|
@@ -440,11 +718,22 @@ class SessionBridgeService {
|
|
|
440
718
|
endSession() {
|
|
441
719
|
const rounds = this.roundsCompleted;
|
|
442
720
|
const id = this.sessionId;
|
|
443
|
-
|
|
444
|
-
this.
|
|
721
|
+
const projectPath = this.projectPath;
|
|
722
|
+
this.waitResolvers = [];
|
|
445
723
|
this.sessionId = null;
|
|
446
724
|
this.projectPath = null;
|
|
447
|
-
this.
|
|
725
|
+
this.applyPipeline = new BridgeBatchPipeline_1.BridgeBatchPipeline({
|
|
726
|
+
kind: 'apply',
|
|
727
|
+
maxQueued: exports.MAX_PENDING_APPLY_BATCHES,
|
|
728
|
+
maxConcurrentLeases: 1,
|
|
729
|
+
queueFullReason: 'apply_queue_full',
|
|
730
|
+
});
|
|
731
|
+
this.variantPipeline = new BridgeBatchPipeline_1.BridgeBatchPipeline({
|
|
732
|
+
kind: 'variant',
|
|
733
|
+
maxQueued: exports.MAX_PENDING_VARIANT_REQUESTS,
|
|
734
|
+
maxConcurrentLeases: exports.MAX_LEASED_VARIANT_BATCHES,
|
|
735
|
+
queueFullReason: 'variant_queue_full',
|
|
736
|
+
});
|
|
448
737
|
this.telemetryContext = null;
|
|
449
738
|
this.roundsCompleted = 0;
|
|
450
739
|
this.feedback = [];
|
|
@@ -462,9 +751,65 @@ class SessionBridgeService {
|
|
|
462
751
|
lastCheckedAt: null,
|
|
463
752
|
startedByRivet: false,
|
|
464
753
|
};
|
|
754
|
+
if (id && projectPath) {
|
|
755
|
+
BridgeQueueStore_1.bridgeQueueStore.delete(projectPath, id);
|
|
756
|
+
}
|
|
465
757
|
log.info(`Session ended: ${id}, ${rounds} rounds completed`);
|
|
758
|
+
this.notifyLivenessChange();
|
|
466
759
|
return rounds;
|
|
467
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Snapshot the real liveness of the change pipeline for stale-watcher
|
|
763
|
+
* detection. `watcherActive` reflects a registered `watch_for_changes` waiter,
|
|
764
|
+
* NOT channel silence; `oldestPendingChangeAgeMs` is measured against `now`
|
|
765
|
+
* (injectable for tests) so a healthy long-poll can never look stuck — only
|
|
766
|
+
* genuinely unconsumed, unwatched work does.
|
|
767
|
+
*/
|
|
768
|
+
getWatcherLiveness(now = Date.now()) {
|
|
769
|
+
const oldestPendingChangeEnqueuedAt = this.oldestPendingChangeEnqueuedAt();
|
|
770
|
+
const pendingCount = this.applyPipeline.queuedDepth() + this.variantPipeline.queuedDepth();
|
|
771
|
+
return {
|
|
772
|
+
watcherActive: this.waitResolvers.length > 0,
|
|
773
|
+
pendingCount,
|
|
774
|
+
oldestPendingChangeEnqueuedAt,
|
|
775
|
+
oldestPendingChangeAgeMs: oldestPendingChangeEnqueuedAt === null
|
|
776
|
+
? null
|
|
777
|
+
: Math.max(0, now - oldestPendingChangeEnqueuedAt),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Subscribe to changes in {@link getWatcherLiveness} state (queue/drain,
|
|
782
|
+
* waiter register/settle, session start/end). Returns an unsubscribe fn.
|
|
783
|
+
*/
|
|
784
|
+
onWatcherLivenessChange(listener) {
|
|
785
|
+
this.livenessListeners.add(listener);
|
|
786
|
+
return () => {
|
|
787
|
+
this.livenessListeners.delete(listener);
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
/** Oldest enqueue time across queued apply and variant batches, or null. */
|
|
791
|
+
oldestPendingChangeEnqueuedAt() {
|
|
792
|
+
const times = [];
|
|
793
|
+
const applyAt = this.applyPipeline.oldestQueuedEnqueuedAt();
|
|
794
|
+
if (applyAt !== null) {
|
|
795
|
+
times.push(applyAt);
|
|
796
|
+
}
|
|
797
|
+
const variantTimes = this.variantPipeline.peekQueuedEnqueuedAt();
|
|
798
|
+
if (variantTimes.length > 0) {
|
|
799
|
+
times.push(variantTimes[0]);
|
|
800
|
+
}
|
|
801
|
+
return times.length > 0 ? Math.min(...times) : null;
|
|
802
|
+
}
|
|
803
|
+
notifyLivenessChange() {
|
|
804
|
+
for (const listener of this.livenessListeners) {
|
|
805
|
+
try {
|
|
806
|
+
listener();
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
log.warn('liveness listener threw', err);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
468
813
|
}
|
|
469
814
|
exports.SessionBridgeService = SessionBridgeService;
|
|
470
815
|
//# sourceMappingURL=SessionBridgeService.js.map
|