newmark-agent 0.6.1 → 0.6.3
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/conversation-utility-host.bundle.cjs +2028 -479
- package/dist/core/agent.d.ts +18 -0
- package/dist/core/agent.js +76 -7
- package/dist/core/agentKernelRunner.js +25 -2
- package/dist/core/browserPdf.d.ts +10 -0
- package/dist/core/browserPdf.js +56 -0
- package/dist/core/browserUse.d.ts +27 -0
- package/dist/core/browserUse.js +33 -5
- package/dist/core/browserUsePageAdapter.d.ts +13 -0
- package/dist/core/browserUsePageAdapter.js +45 -9
- package/dist/core/continuation/contracts.d.ts +197 -0
- package/dist/core/continuation/contracts.js +72 -0
- package/dist/core/continuation/store.d.ts +178 -0
- package/dist/core/continuation/store.js +989 -0
- package/dist/core/conversationKernel.d.ts +80 -1
- package/dist/core/conversationKernel.js +586 -36
- package/dist/core/electronBrowserUseHost.d.ts +4 -0
- package/dist/core/electronBrowserUseHost.js +104 -15
- package/dist/core/requestContextEstimate.d.ts +5 -1
- package/dist/core/searchMcpPool.d.ts +1 -0
- package/dist/core/searchMcpPool.js +20 -7
- package/dist/llm/provider.js +9 -2
- package/dist/main.js +58 -8
- package/dist/providers/chat-completions.adapter.js +6 -1
- package/dist/providers/responses.adapter.js +4 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +25 -2
- package/dist/ui/index.html +244 -84
- package/dist/wsl-agent-host.bundle.cjs +2031 -482
- package/package.json +4 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConversationKernel = void 0;
|
|
4
4
|
const agent_1 = require("./agent");
|
|
5
|
+
const store_1 = require("./continuation/store");
|
|
5
6
|
const crypto_1 = require("crypto");
|
|
6
7
|
const conversationTarget_1 = require("./conversationTarget");
|
|
7
8
|
function changedLineCount(value) {
|
|
@@ -73,20 +74,131 @@ class ConversationKernel {
|
|
|
73
74
|
return runtimes.map(runtime => runtime.runtimeKey);
|
|
74
75
|
}
|
|
75
76
|
/** A Flow and its queue share one Agent; no second history writer is created. */
|
|
76
|
-
beginExternalRun(targetInput, options, createOwner, previousQueuePaused) {
|
|
77
|
+
beginExternalRun(targetInput, options, createOwner, previousQueuePaused, buildInput) {
|
|
77
78
|
const target = this.normalizeTarget(targetInput);
|
|
78
79
|
const existing = this.findRuntime(target);
|
|
79
80
|
if (existing?.activePromise || existing?.externalOwner?.active)
|
|
80
81
|
throw new Error('Target conversation is already running');
|
|
81
82
|
const runtime = existing || this.runtime(target, options, createOwner());
|
|
83
|
+
this.reconcilePendingContinuations(runtime);
|
|
82
84
|
runtime.options = { ...options };
|
|
83
85
|
runtime.externalOwner = { active: true, wasPaused: runtime.externalOwner?.wasPaused ?? previousQueuePaused ?? runtime.queuePaused };
|
|
84
86
|
runtime.queuePaused = true;
|
|
85
87
|
runtime.runId ||= (0, crypto_1.randomUUID)();
|
|
86
88
|
runtime.stopRequestedRunId = '';
|
|
89
|
+
if (buildInput) {
|
|
90
|
+
const continuation = this.continuationStore(runtime);
|
|
91
|
+
if (continuation) {
|
|
92
|
+
if (buildInput.existingBuildId) {
|
|
93
|
+
runtime.externalBuildId = buildInput.existingBuildId;
|
|
94
|
+
runtime.externalScope = continuation.store.claimExternalBuild({
|
|
95
|
+
workspaceId: continuation.workspaceId,
|
|
96
|
+
rootId: continuation.rootId,
|
|
97
|
+
branchId: continuation.branchId,
|
|
98
|
+
buildId: buildInput.existingBuildId,
|
|
99
|
+
workerCapability: 'flow-external',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (buildInput.resume && runtime.externalBuildId) {
|
|
103
|
+
runtime.externalScope = continuation.store.claimExternalBuild({
|
|
104
|
+
workspaceId: continuation.workspaceId,
|
|
105
|
+
rootId: continuation.rootId,
|
|
106
|
+
branchId: continuation.branchId,
|
|
107
|
+
buildId: runtime.externalBuildId,
|
|
108
|
+
workerCapability: 'flow-external',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const authoritative = continuation.store.snapshot({
|
|
113
|
+
workspaceId: continuation.workspaceId,
|
|
114
|
+
rootId: continuation.rootId,
|
|
115
|
+
branchId: continuation.branchId,
|
|
116
|
+
});
|
|
117
|
+
const receipt = continuation.store.enqueueBuild({
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
type: 'ENQUEUE_BUILD',
|
|
120
|
+
commandId: buildInput.commandId || (0, crypto_1.randomUUID)(),
|
|
121
|
+
actorId: 'flow',
|
|
122
|
+
target: {
|
|
123
|
+
workspaceId: continuation.workspaceId,
|
|
124
|
+
expectedRootId: continuation.rootId,
|
|
125
|
+
branchId: continuation.branchId,
|
|
126
|
+
},
|
|
127
|
+
expectedTailBuildId: authoritative.tailBuildId,
|
|
128
|
+
expectedQueueRevision: authoritative.queueRevision,
|
|
129
|
+
input: {
|
|
130
|
+
text: buildInput.text,
|
|
131
|
+
requestedMode: buildInput.requestedMode || 'flow',
|
|
132
|
+
goalObjective: buildInput.goalObjective,
|
|
133
|
+
modelSelection: buildInput.modelSelection || runtime.options.model,
|
|
134
|
+
branchNodeId: buildInput.branchNodeId,
|
|
135
|
+
branchPath: buildInput.branchPath,
|
|
136
|
+
},
|
|
137
|
+
recipeSnapshotId: `${continuation.rootId}:${continuation.branchId}:flow`,
|
|
138
|
+
createdAt: new Date().toISOString(),
|
|
139
|
+
});
|
|
140
|
+
runtime.externalBuildId = receipt.buildId;
|
|
141
|
+
runtime.externalScope = continuation.store.claimExternalBuild({
|
|
142
|
+
workspaceId: continuation.workspaceId,
|
|
143
|
+
rootId: continuation.rootId,
|
|
144
|
+
branchId: continuation.branchId,
|
|
145
|
+
buildId: receipt.buildId,
|
|
146
|
+
workerCapability: 'flow-external',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
87
151
|
this.emitQueueUpdate(runtime);
|
|
88
152
|
return runtime.runner;
|
|
89
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Admit a Flow build at command-acceptance time. The Flow may wait behind a
|
|
156
|
+
* running Build, but its queue position (and therefore the parent of every
|
|
157
|
+
* later Next item) is fixed now, not when the Flow finally starts.
|
|
158
|
+
*/
|
|
159
|
+
admitExternalBuild(targetInput, buildInput) {
|
|
160
|
+
const target = this.normalizeTarget(targetInput);
|
|
161
|
+
const existing = this.findRuntime(target);
|
|
162
|
+
const runtime = existing || this.runtime(target, {
|
|
163
|
+
mode: 'flow',
|
|
164
|
+
model: buildInput.modelSelection || this.host.modelSelectionValue(),
|
|
165
|
+
intelligence: this.host.intelligence,
|
|
166
|
+
inputMode: this.host.inputMode,
|
|
167
|
+
engine: this.host.engine,
|
|
168
|
+
}, this.createRunner(target));
|
|
169
|
+
const continuation = this.continuationStore(runtime);
|
|
170
|
+
if (!continuation)
|
|
171
|
+
return null;
|
|
172
|
+
const authoritative = continuation.store.snapshot({
|
|
173
|
+
workspaceId: continuation.workspaceId,
|
|
174
|
+
rootId: continuation.rootId,
|
|
175
|
+
branchId: continuation.branchId,
|
|
176
|
+
});
|
|
177
|
+
const receipt = continuation.store.enqueueBuild({
|
|
178
|
+
schemaVersion: 1,
|
|
179
|
+
type: 'ENQUEUE_BUILD',
|
|
180
|
+
commandId: buildInput.commandId,
|
|
181
|
+
actorId: 'flow',
|
|
182
|
+
target: {
|
|
183
|
+
workspaceId: continuation.workspaceId,
|
|
184
|
+
expectedRootId: continuation.rootId,
|
|
185
|
+
branchId: continuation.branchId,
|
|
186
|
+
},
|
|
187
|
+
expectedTailBuildId: authoritative.tailBuildId,
|
|
188
|
+
expectedQueueRevision: authoritative.queueRevision,
|
|
189
|
+
input: {
|
|
190
|
+
text: buildInput.text,
|
|
191
|
+
requestedMode: buildInput.requestedMode || 'flow',
|
|
192
|
+
goalObjective: buildInput.goalObjective,
|
|
193
|
+
modelSelection: buildInput.modelSelection || runtime.options.model,
|
|
194
|
+
branchNodeId: buildInput.branchNodeId,
|
|
195
|
+
branchPath: buildInput.branchPath,
|
|
196
|
+
},
|
|
197
|
+
recipeSnapshotId: `${continuation.rootId}:${continuation.branchId}:flow`,
|
|
198
|
+
createdAt: new Date().toISOString(),
|
|
199
|
+
});
|
|
200
|
+
return receipt.buildId;
|
|
201
|
+
}
|
|
90
202
|
settleExternalRun(target, completed) {
|
|
91
203
|
const runtime = this.findRuntime(target);
|
|
92
204
|
if (!runtime?.externalOwner)
|
|
@@ -96,6 +208,46 @@ class ConversationKernel {
|
|
|
96
208
|
runtime.externalOwner = undefined;
|
|
97
209
|
else
|
|
98
210
|
runtime.externalOwner.active = false;
|
|
211
|
+
if (runtime.externalBuildId) {
|
|
212
|
+
const continuation = this.continuationStore(runtime);
|
|
213
|
+
if (continuation) {
|
|
214
|
+
if (completed) {
|
|
215
|
+
if (runtime.externalScope) {
|
|
216
|
+
continuation.store.commitFinal({
|
|
217
|
+
scope: runtime.externalScope,
|
|
218
|
+
text: runtime.externalFinalText || '',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
continuation.store.commitExternalBuild({
|
|
223
|
+
workspaceId: continuation.workspaceId,
|
|
224
|
+
rootId: continuation.rootId,
|
|
225
|
+
branchId: continuation.branchId,
|
|
226
|
+
buildId: runtime.externalBuildId,
|
|
227
|
+
text: runtime.externalFinalText || '',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
runtime.externalBuildId = undefined;
|
|
231
|
+
runtime.externalScope = undefined;
|
|
232
|
+
runtime.externalFinalText = undefined;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Suspend/resume keeps the same durable build identity. Release the
|
|
236
|
+
// current attempt back to QUEUED so the resume can claim a new
|
|
237
|
+
// attempt/fence without creating a second Flow build.
|
|
238
|
+
try {
|
|
239
|
+
continuation.store.requeueBuild({
|
|
240
|
+
workspaceId: continuation.workspaceId,
|
|
241
|
+
rootId: continuation.rootId,
|
|
242
|
+
branchId: continuation.branchId,
|
|
243
|
+
buildId: runtime.externalBuildId,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch { /* an external failure may already have terminated the build */ }
|
|
247
|
+
runtime.externalScope = undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
99
251
|
runtime.queuePaused = completed && !runtime.preparingArchive ? wasPaused : true;
|
|
100
252
|
runtime.runner.saveWorkspaceConversationState(true);
|
|
101
253
|
this.emitQueueUpdate(runtime);
|
|
@@ -108,6 +260,27 @@ class ConversationKernel {
|
|
|
108
260
|
return;
|
|
109
261
|
if (runtime.externalOwner?.active)
|
|
110
262
|
throw new Error('Wait for the Flow to stop before releasing its queue');
|
|
263
|
+
if (runtime.externalBuildId) {
|
|
264
|
+
const continuation = this.continuationStore(runtime);
|
|
265
|
+
if (continuation) {
|
|
266
|
+
// The user explicitly exited the paused Flow and resumed the queue.
|
|
267
|
+
// Record that decision as the Flow build's final instead of leaving a
|
|
268
|
+
// cancelled parent that would permanently block its queued successor.
|
|
269
|
+
try {
|
|
270
|
+
continuation.store.commitExternalBuild({
|
|
271
|
+
workspaceId: continuation.workspaceId,
|
|
272
|
+
rootId: continuation.rootId,
|
|
273
|
+
branchId: continuation.branchId,
|
|
274
|
+
buildId: runtime.externalBuildId,
|
|
275
|
+
text: 'Flow exited by the user; the conversation queue resumed.',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch { /* the Flow build may already be committed or explicitly cancelled */ }
|
|
279
|
+
}
|
|
280
|
+
runtime.externalBuildId = undefined;
|
|
281
|
+
runtime.externalScope = undefined;
|
|
282
|
+
runtime.externalFinalText = undefined;
|
|
283
|
+
}
|
|
111
284
|
runtime.externalOwner = undefined;
|
|
112
285
|
this.setQueuePaused(target, paused);
|
|
113
286
|
}
|
|
@@ -225,6 +398,7 @@ class ConversationKernel {
|
|
|
225
398
|
return [];
|
|
226
399
|
return [{
|
|
227
400
|
id,
|
|
401
|
+
buildId: item.message.buildId,
|
|
228
402
|
text: String(item.message.visibleUserInput || item.message.text || '').replace(/^\[Next queued while current turn is running\]\n/, ''),
|
|
229
403
|
queueMode: item.queueMode,
|
|
230
404
|
requestedMode: item.message.visibleMode,
|
|
@@ -232,9 +406,56 @@ class ConversationKernel {
|
|
|
232
406
|
runId: item.message.runId,
|
|
233
407
|
createdAt: String(item.message.createdAt || ''),
|
|
234
408
|
images: item.message.images?.map(image => ({ ...image })),
|
|
409
|
+
branchNodeId: item.message.branchNodeId,
|
|
410
|
+
branchPath: item.message.branchPath?.slice(),
|
|
411
|
+
modelSelection: item.message.modelSelection,
|
|
235
412
|
}];
|
|
236
413
|
});
|
|
237
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Capture the exact runtime branch that owns a queued user turn. A queued
|
|
417
|
+
* Next is a new user turn, but it must still land on the branch the user was
|
|
418
|
+
* viewing when they enqueued it, not on whatever branch happens to be active
|
|
419
|
+
* when the previous Build settles.
|
|
420
|
+
*/
|
|
421
|
+
runtimeBranchIdentity(runtime) {
|
|
422
|
+
try {
|
|
423
|
+
const snapshot = runtime.runner.getConversationSnapshot(runtime.id);
|
|
424
|
+
const path = Array.isArray(snapshot.runtimeBranchNodePath)
|
|
425
|
+
? snapshot.runtimeBranchNodePath.map(id => String(id || '')).filter(Boolean)
|
|
426
|
+
: [];
|
|
427
|
+
const branchNodeId = String(snapshot.runtimeBranchId || snapshot.activeBranchId || path[path.length - 1] || '');
|
|
428
|
+
return {
|
|
429
|
+
branchNodeId,
|
|
430
|
+
branchPath: path.length ? path : (branchNodeId ? [branchNodeId] : []),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return { branchNodeId: '', branchPath: [] };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Map the current conversation/branch model onto the continuation-v1
|
|
439
|
+
* Root/Branch identity. The ledger is workspace-scoped and shared by the
|
|
440
|
+
* PC GUI, hosted mobile API and isolated runtimes.
|
|
441
|
+
*/
|
|
442
|
+
continuationStore(runtime, branchNodeIdOverride = '') {
|
|
443
|
+
const workspace = runtime.target.workspace;
|
|
444
|
+
if (!workspace?.path)
|
|
445
|
+
return null;
|
|
446
|
+
const workspaceId = String(workspace.id || workspace.path);
|
|
447
|
+
const branchIdentity = this.runtimeBranchIdentity(runtime);
|
|
448
|
+
const rootId = runtime.target.conversationId;
|
|
449
|
+
// Ledger branch ids are workspace-global, while a runtime branch node id
|
|
450
|
+
// is only unique inside its conversation/root. Namespace it by root so two
|
|
451
|
+
// conversations cannot share one authoritative branch record.
|
|
452
|
+
const branchId = branchNodeIdOverride.includes('::')
|
|
453
|
+
? branchNodeIdOverride
|
|
454
|
+
: `${rootId}::${branchNodeIdOverride || branchIdentity.branchNodeId || 'main'}`;
|
|
455
|
+
const store = new store_1.GuardedContinuationStore(workspace.path, workspaceId);
|
|
456
|
+
store.ensureRoot(rootId, branchId);
|
|
457
|
+
return { store, workspaceId, rootId, branchId };
|
|
458
|
+
}
|
|
238
459
|
enqueueNext(target, input) {
|
|
239
460
|
const runtime = this.findRuntime(target);
|
|
240
461
|
if (!runtime)
|
|
@@ -249,8 +470,54 @@ class ConversationKernel {
|
|
|
249
470
|
return existing;
|
|
250
471
|
const prompt = `[Next queued while current turn is running]\n${text}`;
|
|
251
472
|
const createdAt = String(input.createdAt || new Date().toISOString());
|
|
473
|
+
const capturedBranch = this.runtimeBranchIdentity(runtime);
|
|
474
|
+
const branchPath = Array.isArray(input.branchPath) && input.branchPath.length
|
|
475
|
+
? input.branchPath.map(id => String(id || '')).filter(Boolean)
|
|
476
|
+
: capturedBranch.branchPath;
|
|
477
|
+
const branchNodeId = String(input.branchNodeId || branchPath[branchPath.length - 1] || capturedBranch.branchNodeId || '');
|
|
478
|
+
const targetRuntimeKey = String(input.targetRuntimeKey || runtime.target.runtimeKey).trim() || runtime.target.runtimeKey;
|
|
479
|
+
const workspaceKey = runtime.target.workspaceKey;
|
|
480
|
+
const modelSelection = String(input.modelSelection || runtime.options.model || '').trim() || undefined;
|
|
481
|
+
// Gate B/C: admission is an authoritative CAS transaction. The in-memory
|
|
482
|
+
// pendingNextTurn list is only a projection of this durable build.
|
|
483
|
+
const continuation = this.continuationStore(runtime, branchNodeId);
|
|
484
|
+
const ledgerBranchId = continuation?.branchId;
|
|
485
|
+
let buildId = '';
|
|
486
|
+
if (continuation) {
|
|
487
|
+
const authoritative = continuation.store.snapshot({
|
|
488
|
+
workspaceId: continuation.workspaceId,
|
|
489
|
+
rootId: continuation.rootId,
|
|
490
|
+
branchId: continuation.branchId,
|
|
491
|
+
});
|
|
492
|
+
const receipt = continuation.store.enqueueBuild({
|
|
493
|
+
schemaVersion: 1,
|
|
494
|
+
type: 'ENQUEUE_BUILD',
|
|
495
|
+
commandId: id,
|
|
496
|
+
actorId: 'kernel',
|
|
497
|
+
target: {
|
|
498
|
+
workspaceId: continuation.workspaceId,
|
|
499
|
+
expectedRootId: continuation.rootId,
|
|
500
|
+
branchId: continuation.branchId,
|
|
501
|
+
},
|
|
502
|
+
expectedTailBuildId: authoritative.tailBuildId,
|
|
503
|
+
expectedQueueRevision: authoritative.queueRevision,
|
|
504
|
+
input: {
|
|
505
|
+
text,
|
|
506
|
+
images: input.images,
|
|
507
|
+
requestedMode: input.requestedMode,
|
|
508
|
+
goalObjective: input.goalObjective,
|
|
509
|
+
modelSelection,
|
|
510
|
+
branchNodeId: branchNodeId || undefined,
|
|
511
|
+
branchPath: branchPath.length ? branchPath : undefined,
|
|
512
|
+
},
|
|
513
|
+
recipeSnapshotId: `${continuation.rootId}:${continuation.branchId}:${modelSelection || 'auto'}`,
|
|
514
|
+
createdAt,
|
|
515
|
+
});
|
|
516
|
+
buildId = receipt.buildId;
|
|
517
|
+
}
|
|
252
518
|
runtime.pendingNextTurn.push({
|
|
253
519
|
message: {
|
|
520
|
+
buildId: buildId || undefined,
|
|
254
521
|
text: prompt,
|
|
255
522
|
visibleUserInput: text,
|
|
256
523
|
visibleMode: String(input.requestedMode || 'build'),
|
|
@@ -258,11 +525,18 @@ class ConversationKernel {
|
|
|
258
525
|
images: Array.isArray(input.images) ? input.images.slice(0, 6).map(image => ({ ...image })) : undefined,
|
|
259
526
|
clientMessageId: id,
|
|
260
527
|
runId: runtime.runId,
|
|
528
|
+
branchNodeId: branchNodeId || undefined,
|
|
529
|
+
branchPath: branchPath.length ? branchPath : undefined,
|
|
530
|
+
ledgerBranchId,
|
|
531
|
+
targetRuntimeKey,
|
|
532
|
+
workspaceKey,
|
|
533
|
+
modelSelection,
|
|
261
534
|
createdAt,
|
|
262
535
|
},
|
|
263
536
|
queueMode: 'followUp',
|
|
264
537
|
});
|
|
265
538
|
runtime.runner.retainConversationContinuations([{
|
|
539
|
+
buildId: buildId || undefined,
|
|
266
540
|
content: prompt,
|
|
267
541
|
queueMode: 'followUp',
|
|
268
542
|
clientMessageId: id,
|
|
@@ -272,6 +546,12 @@ class ConversationKernel {
|
|
|
272
546
|
visibleUserInput: text,
|
|
273
547
|
visibleMode: String(input.requestedMode || 'build'),
|
|
274
548
|
goalObjective: input.goalObjective,
|
|
549
|
+
branchNodeId: branchNodeId || undefined,
|
|
550
|
+
branchPath: branchPath.length ? branchPath : undefined,
|
|
551
|
+
ledgerBranchId,
|
|
552
|
+
targetRuntimeKey,
|
|
553
|
+
workspaceKey,
|
|
554
|
+
modelSelection,
|
|
275
555
|
}]);
|
|
276
556
|
this.trackQueuedMessage(runtime, prompt, 'followUp');
|
|
277
557
|
this.emitQueueUpdate(runtime);
|
|
@@ -291,14 +571,35 @@ class ConversationKernel {
|
|
|
291
571
|
if (!pending || typeof pending.message === 'string')
|
|
292
572
|
throw new Error('Queue item is no longer editable');
|
|
293
573
|
const previous = runtime.runner.conversationContinuations().find(item => item.clientMessageId === id);
|
|
294
|
-
|
|
574
|
+
const nextText = `[Next queued while current turn is running]\n${text}`;
|
|
575
|
+
const nextVisibleMode = input.requestedMode !== undefined ? input.requestedMode : pending.message.visibleMode;
|
|
576
|
+
const nextGoalObjective = input.goalObjective !== undefined ? input.goalObjective : pending.message.goalObjective;
|
|
577
|
+
const nextImages = input.images !== undefined
|
|
578
|
+
? input.images.map(image => ({ ...image }))
|
|
579
|
+
: pending.message.images?.map(image => ({ ...image }));
|
|
580
|
+
const continuation = this.continuationStore(runtime, pending.message.ledgerBranchId || pending.message.branchNodeId);
|
|
581
|
+
if (continuation && pending.message.buildId) {
|
|
582
|
+
continuation.store.updateQueuedBuild({
|
|
583
|
+
workspaceId: continuation.workspaceId,
|
|
584
|
+
rootId: continuation.rootId,
|
|
585
|
+
branchId: continuation.branchId,
|
|
586
|
+
buildId: pending.message.buildId,
|
|
587
|
+
input: {
|
|
588
|
+
text,
|
|
589
|
+
images: nextImages,
|
|
590
|
+
requestedMode: nextVisibleMode,
|
|
591
|
+
goalObjective: nextGoalObjective,
|
|
592
|
+
modelSelection: pending.message.modelSelection,
|
|
593
|
+
branchNodeId: pending.message.branchNodeId,
|
|
594
|
+
branchPath: pending.message.branchPath,
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
pending.message.text = nextText;
|
|
295
599
|
pending.message.visibleUserInput = text;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
pending.message.goalObjective = input.goalObjective;
|
|
300
|
-
if (input.images !== undefined)
|
|
301
|
-
pending.message.images = input.images.map(image => ({ ...image }));
|
|
600
|
+
pending.message.visibleMode = nextVisibleMode;
|
|
601
|
+
pending.message.goalObjective = nextGoalObjective;
|
|
602
|
+
pending.message.images = nextImages;
|
|
302
603
|
runtime.runner.retainConversationContinuations([{
|
|
303
604
|
...previous,
|
|
304
605
|
content: pending.message.text,
|
|
@@ -309,6 +610,11 @@ class ConversationKernel {
|
|
|
309
610
|
visibleUserInput: text,
|
|
310
611
|
visibleMode: pending.message.visibleMode,
|
|
311
612
|
goalObjective: pending.message.goalObjective,
|
|
613
|
+
branchNodeId: pending.message.branchNodeId,
|
|
614
|
+
branchPath: pending.message.branchPath,
|
|
615
|
+
targetRuntimeKey: pending.message.targetRuntimeKey,
|
|
616
|
+
workspaceKey: pending.message.workspaceKey,
|
|
617
|
+
modelSelection: pending.message.modelSelection,
|
|
312
618
|
}]);
|
|
313
619
|
this.emitQueueUpdate(runtime);
|
|
314
620
|
return this.queueItems(runtime.target).find(item => item.id === id);
|
|
@@ -321,7 +627,19 @@ class ConversationKernel {
|
|
|
321
627
|
const index = runtime.pendingNextTurn.findIndex(item => typeof item.message !== 'string' && item.message.clientMessageId === id);
|
|
322
628
|
if (index < 0)
|
|
323
629
|
return false;
|
|
324
|
-
const
|
|
630
|
+
const removed = runtime.pendingNextTurn[index];
|
|
631
|
+
if (typeof removed.message !== 'string') {
|
|
632
|
+
const continuation = this.continuationStore(runtime, removed.message.ledgerBranchId || removed.message.branchNodeId);
|
|
633
|
+
if (continuation && removed.message.buildId) {
|
|
634
|
+
continuation.store.deleteQueuedBuild({
|
|
635
|
+
workspaceId: continuation.workspaceId,
|
|
636
|
+
rootId: continuation.rootId,
|
|
637
|
+
branchId: continuation.branchId,
|
|
638
|
+
buildId: removed.message.buildId,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
runtime.pendingNextTurn.splice(index, 1);
|
|
325
643
|
if (typeof removed.message !== 'string') {
|
|
326
644
|
runtime.runner.consumeConversationContinuation({ content: removed.message.text, queueMode: removed.queueMode, clientMessageId: id });
|
|
327
645
|
}
|
|
@@ -355,6 +673,27 @@ class ConversationKernel {
|
|
|
355
673
|
return [];
|
|
356
674
|
return [[String(item.message.clientMessageId), item]];
|
|
357
675
|
}));
|
|
676
|
+
const firstOrdered = pendingById.get(orderedIds[0] || '');
|
|
677
|
+
const continuation = this.continuationStore(runtime, firstOrdered && typeof firstOrdered.message !== 'string'
|
|
678
|
+
? (firstOrdered.message.ledgerBranchId || firstOrdered.message.branchNodeId || '')
|
|
679
|
+
: '');
|
|
680
|
+
if (continuation) {
|
|
681
|
+
const orderedBuildIds = orderedIds
|
|
682
|
+
.map(id => {
|
|
683
|
+
const item = pendingById.get(id);
|
|
684
|
+
return item && typeof item.message !== 'string' ? item.message.buildId : undefined;
|
|
685
|
+
})
|
|
686
|
+
.filter((id) => !!id);
|
|
687
|
+
if (orderedBuildIds.length !== orderedIds.length) {
|
|
688
|
+
throw new Error('A queued build id is missing from the authoritative ledger projection');
|
|
689
|
+
}
|
|
690
|
+
continuation.store.reorderQueuedBuilds({
|
|
691
|
+
workspaceId: continuation.workspaceId,
|
|
692
|
+
rootId: continuation.rootId,
|
|
693
|
+
branchId: continuation.branchId,
|
|
694
|
+
orderedBuildIds,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
358
697
|
const reorderedPending = orderedIds.map(id => pendingById.get(id));
|
|
359
698
|
let nextIndex = 0;
|
|
360
699
|
runtime.pendingNextTurn = runtime.pendingNextTurn.map(item => {
|
|
@@ -397,6 +736,10 @@ class ConversationKernel {
|
|
|
397
736
|
goalObjective: input.goalObjective,
|
|
398
737
|
createdAt: input.createdAt,
|
|
399
738
|
images: input.images,
|
|
739
|
+
branchNodeId: input.branchNodeId,
|
|
740
|
+
branchPath: input.branchPath,
|
|
741
|
+
modelSelection: input.modelSelection,
|
|
742
|
+
targetRuntimeKey: input.targetRuntimeKey,
|
|
400
743
|
});
|
|
401
744
|
}
|
|
402
745
|
else if (action === 'update') {
|
|
@@ -948,7 +1291,7 @@ class ConversationKernel {
|
|
|
948
1291
|
// carry no send-time model/mode; the running block keeps its settings and
|
|
949
1292
|
// the next dequeue follows the current conversation selection (which
|
|
950
1293
|
// setModel/setMode already recorded on runtime.options).
|
|
951
|
-
this.enqueueSameSession(active, message, queueMode);
|
|
1294
|
+
this.enqueueSameSession(active, message, queueMode, options.model);
|
|
952
1295
|
this.activateAcceptedGoal(active, typeof message === 'string' ? '' : message.goalObjective);
|
|
953
1296
|
return active.activePromise;
|
|
954
1297
|
}
|
|
@@ -1071,11 +1414,17 @@ class ConversationKernel {
|
|
|
1071
1414
|
return this.result(runtime, lastTokens);
|
|
1072
1415
|
}
|
|
1073
1416
|
for (;;) {
|
|
1074
|
-
while (!runtime.queuePaused
|
|
1417
|
+
while (!runtime.queuePaused) {
|
|
1075
1418
|
if (runtime.stopRequestedRunId === runtime.runId)
|
|
1076
1419
|
return this.result(runtime, lastTokens);
|
|
1077
|
-
|
|
1078
|
-
|
|
1420
|
+
// Only intervening Guides run inside the current Build. A Next item
|
|
1421
|
+
// is a new user turn and stays exactly where the user placed it; the
|
|
1422
|
+
// run finalizer starts a fresh Build for it after this one settles.
|
|
1423
|
+
const steerIndex = runtime.pendingNextTurn.findIndex(item => item.queueMode === 'steer');
|
|
1424
|
+
if (steerIndex < 0)
|
|
1425
|
+
break;
|
|
1426
|
+
const next = runtime.pendingNextTurn.splice(steerIndex, 1)[0];
|
|
1427
|
+
if (typeof next.message !== 'string' && !!next.message.clientMessageId) {
|
|
1079
1428
|
const batchGuides = [];
|
|
1080
1429
|
const pushGuide = (message) => {
|
|
1081
1430
|
batchGuides.push({
|
|
@@ -1087,11 +1436,13 @@ class ConversationKernel {
|
|
|
1087
1436
|
});
|
|
1088
1437
|
};
|
|
1089
1438
|
pushGuide(next.message);
|
|
1090
|
-
while (
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1439
|
+
while (true) {
|
|
1440
|
+
const guideIndex = runtime.pendingNextTurn.findIndex(item => item.queueMode === 'steer'
|
|
1441
|
+
&& typeof item.message !== 'string'
|
|
1442
|
+
&& !!item.message.clientMessageId);
|
|
1443
|
+
if (guideIndex < 0)
|
|
1444
|
+
break;
|
|
1445
|
+
const guide = runtime.pendingNextTurn.splice(guideIndex, 1)[0];
|
|
1095
1446
|
pushGuide(guide.message);
|
|
1096
1447
|
}
|
|
1097
1448
|
if (batchGuides.length === 1) {
|
|
@@ -1111,17 +1462,10 @@ class ConversationKernel {
|
|
|
1111
1462
|
break;
|
|
1112
1463
|
}
|
|
1113
1464
|
else {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (typeof drained !== 'string' && drained.visibleMode && ['build', 'chat', 'plan', 'goal'].includes(drained.visibleMode)) {
|
|
1117
|
-
runtime.runner.setMode(drained.visibleMode);
|
|
1118
|
-
runtime.options.mode = drained.visibleMode;
|
|
1119
|
-
}
|
|
1120
|
-
this.activateAcceptedGoal(runtime, typeof drained === 'string' ? '' : drained.goalObjective);
|
|
1121
|
-
return this.runSingle(runtime, drained, next.queueMode, typeof next.message === 'string' ? undefined : next.message.clientMessageId);
|
|
1122
|
-
});
|
|
1123
|
-
if (this.repeatedAutomaticAssistant(runtime, drained, next.queueMode))
|
|
1465
|
+
lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
|
|
1466
|
+
if (this.repeatedAutomaticAssistant(runtime, next.message, next.queueMode))
|
|
1124
1467
|
break;
|
|
1468
|
+
continue;
|
|
1125
1469
|
}
|
|
1126
1470
|
}
|
|
1127
1471
|
const rootMessage = runtime.runner.subagents.readRootInbox()[0];
|
|
@@ -1144,7 +1488,7 @@ class ConversationKernel {
|
|
|
1144
1488
|
if (runtime.runId === completedRunId
|
|
1145
1489
|
&& runtime.stopRequestedRunId !== completedRunId
|
|
1146
1490
|
&& !runtime.queuePaused
|
|
1147
|
-
&& runtime.pendingNextTurn.
|
|
1491
|
+
&& runtime.pendingNextTurn.some(item => item.queueMode === 'steer')) {
|
|
1148
1492
|
runtime.guideAcceptanceClosedRunId = '';
|
|
1149
1493
|
if (!runtime.runner.resumeConversationWorkRun(completedRunId)) {
|
|
1150
1494
|
throw new Error(`Unable to resume deferred Guide work run ${completedRunId}`);
|
|
@@ -1343,6 +1687,12 @@ class ConversationKernel {
|
|
|
1343
1687
|
visibleUserInput: continuation.visibleUserInput,
|
|
1344
1688
|
visibleMode: continuation.visibleMode,
|
|
1345
1689
|
goalObjective: continuation.goalObjective,
|
|
1690
|
+
buildId: continuation.buildId,
|
|
1691
|
+
modelSelection: continuation.modelSelection,
|
|
1692
|
+
branchNodeId: continuation.branchNodeId,
|
|
1693
|
+
branchPath: continuation.branchPath?.slice(),
|
|
1694
|
+
targetRuntimeKey: continuation.targetRuntimeKey,
|
|
1695
|
+
workspaceKey: continuation.workspaceKey,
|
|
1346
1696
|
createdAt: continuation.createdAt,
|
|
1347
1697
|
hiddenUserInput: continuation.hiddenUserInput,
|
|
1348
1698
|
}
|
|
@@ -1454,13 +1804,110 @@ class ConversationKernel {
|
|
|
1454
1804
|
runtime.pendingContinuationRunId = undefined;
|
|
1455
1805
|
if (runtime.preparingArchive || runtime.runId !== runId || runtime.activePromise || runtime.externalOwner || runtime.stopRequestedRunId === runId || runtime.queuePaused)
|
|
1456
1806
|
return;
|
|
1457
|
-
const next = runtime.pendingNextTurn
|
|
1807
|
+
const next = runtime.pendingNextTurn[0];
|
|
1458
1808
|
if (!next)
|
|
1459
1809
|
return;
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1810
|
+
const queued = typeof next.message === 'string' ? null : next.message;
|
|
1811
|
+
if (queued?.targetRuntimeKey && queued.targetRuntimeKey !== runtime.target.runtimeKey) {
|
|
1812
|
+
runtime.queuePaused = true;
|
|
1813
|
+
runtime.runner.recordWorkStatus('Queued turn target mismatch; the queue is paused instead of running in another conversation.');
|
|
1814
|
+
this.emitQueueUpdate(runtime);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
runtime.pendingNextTurn.shift();
|
|
1818
|
+
if (next.queueMode === 'followUp') {
|
|
1819
|
+
// A Next item is a fresh user turn. It must start a new Build in this
|
|
1820
|
+
// exact conversation after the previous one has fully settled, and it
|
|
1821
|
+
// must land on the branch that owned it when the user enqueued it.
|
|
1822
|
+
// Never carry the completed runId into the new turn.
|
|
1823
|
+
const delivery = this.drainQueuedFollowUpMessage(next.message);
|
|
1824
|
+
const message = typeof delivery === 'string'
|
|
1825
|
+
? { text: delivery }
|
|
1826
|
+
: { ...delivery, runId: undefined };
|
|
1827
|
+
const continuation = this.continuationStore(runtime);
|
|
1828
|
+
let scope = null;
|
|
1829
|
+
void this.runPendingQueueItem(runtime, next, async () => {
|
|
1830
|
+
this.bindQueuedTurnBranch(runtime, queued);
|
|
1831
|
+
if (queued?.modelSelection)
|
|
1832
|
+
runtime.options.model = queued.modelSelection;
|
|
1833
|
+
if (message.visibleMode && ['build', 'chat', 'plan', 'goal'].includes(message.visibleMode)) {
|
|
1834
|
+
this.setMode(runtime.target, message.visibleMode);
|
|
1835
|
+
}
|
|
1836
|
+
this.activateAcceptedGoal(runtime, message.goalObjective || '');
|
|
1837
|
+
if (continuation && queued?.buildId) {
|
|
1838
|
+
const authoritative = continuation.store.snapshot({
|
|
1839
|
+
workspaceId: continuation.workspaceId,
|
|
1840
|
+
rootId: continuation.rootId,
|
|
1841
|
+
branchId: continuation.branchId,
|
|
1842
|
+
});
|
|
1843
|
+
const ready = authoritative.builds
|
|
1844
|
+
.filter(build => build.status === 'QUEUED')
|
|
1845
|
+
.sort((a, b) => a.queueSequence - b.queueSequence)[0];
|
|
1846
|
+
if (ready && ready.buildId !== queued.buildId) {
|
|
1847
|
+
// The in-memory projection is not the authoritative next build.
|
|
1848
|
+
// Keep this row queued instead of executing out of order.
|
|
1849
|
+
runtime.pendingNextTurn.unshift(next);
|
|
1850
|
+
runtime.runner.recordWorkStatus('Queued turn is waiting for the authoritative predecessor.');
|
|
1851
|
+
this.emitQueueUpdate(runtime);
|
|
1852
|
+
return this.result(runtime, []);
|
|
1853
|
+
}
|
|
1854
|
+
scope = continuation.store.claimBuild({
|
|
1855
|
+
workspaceId: continuation.workspaceId,
|
|
1856
|
+
rootId: continuation.rootId,
|
|
1857
|
+
branchId: continuation.branchId,
|
|
1858
|
+
workerCapability: `kernel:${process.pid}`,
|
|
1859
|
+
});
|
|
1860
|
+
if (scope && scope.buildId !== queued.buildId) {
|
|
1861
|
+
continuation.store.failBuild({ scope, reason: 'Authoritative build mismatch during claim' });
|
|
1862
|
+
scope = null;
|
|
1863
|
+
}
|
|
1864
|
+
if (!scope) {
|
|
1865
|
+
runtime.pendingNextTurn.unshift(next);
|
|
1866
|
+
runtime.runner.recordWorkStatus('Queued turn is not ready for execution yet.');
|
|
1867
|
+
this.emitQueueUpdate(runtime);
|
|
1868
|
+
return this.result(runtime, []);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
const result = await this.prompt(message, runtime.target, runtime.options, 'followUp');
|
|
1872
|
+
if (continuation && scope) {
|
|
1873
|
+
continuation.store.commitFinal({
|
|
1874
|
+
scope,
|
|
1875
|
+
text: result.tokens.map(token => token.text || '').join(''),
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
return result;
|
|
1879
|
+
}, (accepted, error) => {
|
|
1880
|
+
if (!continuation || !scope)
|
|
1881
|
+
return;
|
|
1882
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1883
|
+
if (accepted) {
|
|
1884
|
+
try {
|
|
1885
|
+
continuation.store.failBuild({ scope, reason });
|
|
1886
|
+
}
|
|
1887
|
+
catch { /* stop/archive may have already released the guard */ }
|
|
1888
|
+
}
|
|
1889
|
+
else {
|
|
1890
|
+
// The input was never accepted. Stop/archive may already have
|
|
1891
|
+
// released the guard, so use the idempotent requeue path: the same
|
|
1892
|
+
// durable build id stays available for an explicit resume.
|
|
1893
|
+
try {
|
|
1894
|
+
continuation.store.requeueBuild({
|
|
1895
|
+
workspaceId: continuation.workspaceId,
|
|
1896
|
+
rootId: continuation.rootId,
|
|
1897
|
+
branchId: continuation.branchId,
|
|
1898
|
+
buildId: scope.buildId,
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
catch { /* the build may already be committed or explicitly cancelled */ }
|
|
1902
|
+
}
|
|
1903
|
+
}).catch(() => {
|
|
1904
|
+
// Agent.process and the work-run finalizer already publish the error.
|
|
1905
|
+
});
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
const message = typeof next.message === 'string'
|
|
1909
|
+
? { text: next.message, runId }
|
|
1910
|
+
: { ...next.message, runId: next.message.runId || runId };
|
|
1464
1911
|
void this.runPendingQueueItem(runtime, next, async () => {
|
|
1465
1912
|
if (message.visibleMode && ['build', 'chat', 'plan', 'goal'].includes(message.visibleMode)) {
|
|
1466
1913
|
this.setMode(runtime.target, message.visibleMode);
|
|
@@ -1471,7 +1918,27 @@ class ConversationKernel {
|
|
|
1471
1918
|
});
|
|
1472
1919
|
});
|
|
1473
1920
|
}
|
|
1474
|
-
|
|
1921
|
+
/**
|
|
1922
|
+
* Restore the runtime branch captured by a queued user turn. If the branch
|
|
1923
|
+
* disappeared (manual deletion/rewind), fail the queued item into the
|
|
1924
|
+
* existing pause/retry path instead of appending it to a different branch.
|
|
1925
|
+
*/
|
|
1926
|
+
bindQueuedTurnBranch(runtime, message) {
|
|
1927
|
+
const branchNodeId = String(message?.branchNodeId || '').trim();
|
|
1928
|
+
if (!branchNodeId)
|
|
1929
|
+
return;
|
|
1930
|
+
const snapshot = runtime.runner.getConversationSnapshot(runtime.id);
|
|
1931
|
+
const currentBranch = String(snapshot.runtimeBranchId || snapshot.activeBranchId || '');
|
|
1932
|
+
if (currentBranch === branchNodeId)
|
|
1933
|
+
return;
|
|
1934
|
+
try {
|
|
1935
|
+
runtime.runner.switchConversationBranch(runtime.id, branchNodeId);
|
|
1936
|
+
}
|
|
1937
|
+
catch (error) {
|
|
1938
|
+
throw new Error(`Queued turn branch ${branchNodeId} is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
async runPendingQueueItem(runtime, next, execute, onFailure) {
|
|
1475
1942
|
const queuedMessage = typeof next.message === 'string' ? undefined : next.message;
|
|
1476
1943
|
const id = queuedMessage?.clientMessageId;
|
|
1477
1944
|
if (next.queueMode !== 'followUp' || !id)
|
|
@@ -1491,6 +1958,10 @@ class ConversationKernel {
|
|
|
1491
1958
|
// the same row manageable. A provider failure after acceptance must
|
|
1492
1959
|
// never replay the already submitted user input after restart/resume.
|
|
1493
1960
|
runtime.queuePaused = true;
|
|
1961
|
+
try {
|
|
1962
|
+
onFailure?.(accepted, error);
|
|
1963
|
+
}
|
|
1964
|
+
catch { /* the original failure is authoritative */ }
|
|
1494
1965
|
throw error;
|
|
1495
1966
|
}
|
|
1496
1967
|
finally {
|
|
@@ -1669,13 +2140,26 @@ class ConversationKernel {
|
|
|
1669
2140
|
guides: (run.guides || []).map(bindGuide),
|
|
1670
2141
|
}));
|
|
1671
2142
|
}
|
|
1672
|
-
enqueueSameSession(runtime, message, queueMode) {
|
|
2143
|
+
enqueueSameSession(runtime, message, queueMode, requestedModel = '') {
|
|
1673
2144
|
const isSteer = queueMode === 'steer';
|
|
1674
2145
|
const text = typeof message === 'string' ? message : message.text;
|
|
1675
2146
|
const prompt = isSteer ? text : `[Next queued while current turn is running]\n${text}`;
|
|
1676
2147
|
if (!isSteer) {
|
|
1677
2148
|
const structured = typeof message === 'string' ? null : message;
|
|
2149
|
+
const requestedRuntimeKey = String(structured?.targetRuntimeKey || '').trim();
|
|
2150
|
+
if (requestedRuntimeKey && requestedRuntimeKey !== runtime.target.runtimeKey) {
|
|
2151
|
+
throw new Error(`Queued message target mismatch: expected ${requestedRuntimeKey}, received ${runtime.target.runtimeKey}`);
|
|
2152
|
+
}
|
|
1678
2153
|
const clientMessageId = String(structured?.clientMessageId || (0, crypto_1.randomUUID)());
|
|
2154
|
+
const capturedBranch = this.runtimeBranchIdentity(runtime);
|
|
2155
|
+
const branchPath = Array.isArray(structured?.branchPath) && structured.branchPath.length
|
|
2156
|
+
? structured.branchPath.map(id => String(id || '')).filter(Boolean)
|
|
2157
|
+
: capturedBranch.branchPath;
|
|
2158
|
+
const branchNodeId = String(structured?.branchNodeId || branchPath[branchPath.length - 1] || capturedBranch.branchNodeId || '');
|
|
2159
|
+
const targetRuntimeKey = runtime.target.runtimeKey;
|
|
2160
|
+
const workspaceKey = runtime.target.workspaceKey;
|
|
2161
|
+
const modelSelection = String((structured?.modelSelection) || requestedModel || runtime.options.model || '').trim() || undefined;
|
|
2162
|
+
const ledgerBranchId = this.continuationStore(runtime, branchNodeId)?.branchId;
|
|
1679
2163
|
const queuedMessage = {
|
|
1680
2164
|
...(structured || {}),
|
|
1681
2165
|
text: prompt,
|
|
@@ -1683,6 +2167,12 @@ class ConversationKernel {
|
|
|
1683
2167
|
visibleMode: structured?.visibleMode || runtime.options.mode,
|
|
1684
2168
|
clientMessageId,
|
|
1685
2169
|
runId: structured?.runId || runtime.runId,
|
|
2170
|
+
branchNodeId: branchNodeId || undefined,
|
|
2171
|
+
branchPath: branchPath.length ? branchPath : undefined,
|
|
2172
|
+
ledgerBranchId,
|
|
2173
|
+
targetRuntimeKey,
|
|
2174
|
+
workspaceKey,
|
|
2175
|
+
modelSelection,
|
|
1686
2176
|
createdAt: String(structured?.createdAt || new Date().toISOString()),
|
|
1687
2177
|
};
|
|
1688
2178
|
if (!runtime.pendingNextTurn.some(item => typeof item.message !== 'string' && item.message.clientMessageId === clientMessageId)) {
|
|
@@ -1699,6 +2189,12 @@ class ConversationKernel {
|
|
|
1699
2189
|
visibleUserInput: queuedMessage.visibleUserInput,
|
|
1700
2190
|
visibleMode: queuedMessage.visibleMode,
|
|
1701
2191
|
goalObjective: queuedMessage.goalObjective,
|
|
2192
|
+
branchNodeId: queuedMessage.branchNodeId,
|
|
2193
|
+
branchPath: queuedMessage.branchPath,
|
|
2194
|
+
ledgerBranchId: queuedMessage.ledgerBranchId,
|
|
2195
|
+
targetRuntimeKey,
|
|
2196
|
+
workspaceKey,
|
|
2197
|
+
modelSelection,
|
|
1702
2198
|
}]);
|
|
1703
2199
|
this.trackQueuedMessage(runtime, prompt, 'followUp');
|
|
1704
2200
|
runtime.runner.recordWorkStatus(runtime.stopRequestedRunId === runtime.runId
|
|
@@ -1799,6 +2295,60 @@ class ConversationKernel {
|
|
|
1799
2295
|
.map(item => typeof item.message === 'string' ? item.message : item.message.text);
|
|
1800
2296
|
return runtime;
|
|
1801
2297
|
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Merge queue continuations persisted by another owner/kernel into this
|
|
2300
|
+
* runtime's in-memory projection. Flow takeover, cold restore and a pool
|
|
2301
|
+
* hand-off must not lose an already-admitted Next row. Deduplication is by
|
|
2302
|
+
* the stable clientMessageId, never by text.
|
|
2303
|
+
*/
|
|
2304
|
+
reconcilePendingContinuations(runtime) {
|
|
2305
|
+
// The other owner (pool/flow/cold restore) commits queue continuations to
|
|
2306
|
+
// the workspace state file. Reload this runner's durable view before
|
|
2307
|
+
// merging so a takeover cannot miss a row persisted after the runner was
|
|
2308
|
+
// first created.
|
|
2309
|
+
runtime.runner.setConversationFromStorage(runtime.id);
|
|
2310
|
+
const persisted = runtime.runner.conversationContinuations();
|
|
2311
|
+
const existingIds = new Set();
|
|
2312
|
+
for (const item of runtime.pendingNextTurn) {
|
|
2313
|
+
if (typeof item.message === 'string')
|
|
2314
|
+
continue;
|
|
2315
|
+
const id = String(item.message.clientMessageId || '').trim();
|
|
2316
|
+
if (id)
|
|
2317
|
+
existingIds.add(id);
|
|
2318
|
+
}
|
|
2319
|
+
let changed = false;
|
|
2320
|
+
for (const continuation of persisted) {
|
|
2321
|
+
if (continuation.queueMode !== 'followUp' || !continuation.clientMessageId)
|
|
2322
|
+
continue;
|
|
2323
|
+
if (existingIds.has(continuation.clientMessageId))
|
|
2324
|
+
continue;
|
|
2325
|
+
runtime.pendingNextTurn.push({
|
|
2326
|
+
message: {
|
|
2327
|
+
text: continuation.content,
|
|
2328
|
+
visibleUserInput: continuation.visibleUserInput,
|
|
2329
|
+
visibleMode: continuation.visibleMode,
|
|
2330
|
+
goalObjective: continuation.goalObjective,
|
|
2331
|
+
images: continuation.images?.map(image => ({ ...image })),
|
|
2332
|
+
attachments: continuation.attachments?.map(attachment => ({ ...attachment })),
|
|
2333
|
+
clientMessageId: continuation.clientMessageId,
|
|
2334
|
+
buildId: continuation.buildId,
|
|
2335
|
+
branchNodeId: continuation.branchNodeId,
|
|
2336
|
+
branchPath: continuation.branchPath?.slice(),
|
|
2337
|
+
targetRuntimeKey: continuation.targetRuntimeKey,
|
|
2338
|
+
workspaceKey: continuation.workspaceKey,
|
|
2339
|
+
modelSelection: continuation.modelSelection,
|
|
2340
|
+
createdAt: continuation.createdAt,
|
|
2341
|
+
},
|
|
2342
|
+
queueMode: 'followUp',
|
|
2343
|
+
});
|
|
2344
|
+
existingIds.add(continuation.clientMessageId);
|
|
2345
|
+
changed = true;
|
|
2346
|
+
}
|
|
2347
|
+
if (changed) {
|
|
2348
|
+
this.queueState(runtime);
|
|
2349
|
+
this.emitQueueUpdate(runtime);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
1802
2352
|
normalizeTarget(input) {
|
|
1803
2353
|
if (typeof input !== 'string') {
|
|
1804
2354
|
const withWorkspace = input.workspace ? input : { ...input, workspace: this.workspaceForId(input.workspaceId) };
|