newmark-agent 0.6.2 → 0.6.4
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 +2149 -475
- package/dist/core/agent.d.ts +37 -0
- package/dist/core/agent.js +104 -7
- package/dist/core/agentKernelRunner.js +37 -2
- package/dist/core/branchIdentity.d.ts +29 -0
- package/dist/core/branchIdentity.js +54 -0
- package/dist/core/continuation/contracts.d.ts +197 -0
- package/dist/core/continuation/contracts.js +72 -0
- package/dist/core/continuation/store.d.ts +206 -0
- package/dist/core/continuation/store.js +1077 -0
- package/dist/core/conversationKernel.d.ts +93 -2
- package/dist/core/conversationKernel.js +648 -36
- package/dist/core/requestContextEstimate.d.ts +5 -1
- package/dist/llm/provider.js +9 -2
- package/dist/main.js +40 -5
- 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/ui/index.html +413 -55
- package/dist/wsl-agent-host.bundle.cjs +2152 -478
- package/package.json +5 -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
|
}
|
|
@@ -215,6 +388,23 @@ class ConversationKernel {
|
|
|
215
388
|
const runtime = this.findRuntime(target);
|
|
216
389
|
if (!runtime)
|
|
217
390
|
return [];
|
|
391
|
+
// dev-0.6.4 hotfix: 把账本里的受阻原因带进队列投影,界面才能显示
|
|
392
|
+
// “被失败前置阻断”而不是无声卡住。
|
|
393
|
+
let waitingByBuildId = new Map();
|
|
394
|
+
const continuation = this.continuationStore(runtime);
|
|
395
|
+
if (continuation) {
|
|
396
|
+
try {
|
|
397
|
+
const snapshot = continuation.store.snapshot({
|
|
398
|
+
workspaceId: continuation.workspaceId,
|
|
399
|
+
rootId: continuation.rootId,
|
|
400
|
+
branchId: continuation.branchId,
|
|
401
|
+
});
|
|
402
|
+
waitingByBuildId = new Map(snapshot.builds
|
|
403
|
+
.filter(build => !!build.waitingReason)
|
|
404
|
+
.map(build => [build.buildId, String(build.waitingReason)]));
|
|
405
|
+
}
|
|
406
|
+
catch { /* the projection stays usable without ledger diagnostics */ }
|
|
407
|
+
}
|
|
218
408
|
return runtime.pendingNextTurn.flatMap(item => {
|
|
219
409
|
if (item.queueMode !== 'followUp')
|
|
220
410
|
return [];
|
|
@@ -223,8 +413,10 @@ class ConversationKernel {
|
|
|
223
413
|
const id = String(item.message.clientMessageId || '');
|
|
224
414
|
if (!id)
|
|
225
415
|
return [];
|
|
416
|
+
const buildId = String(item.message.buildId || '');
|
|
226
417
|
return [{
|
|
227
418
|
id,
|
|
419
|
+
buildId: item.message.buildId,
|
|
228
420
|
text: String(item.message.visibleUserInput || item.message.text || '').replace(/^\[Next queued while current turn is running\]\n/, ''),
|
|
229
421
|
queueMode: item.queueMode,
|
|
230
422
|
requestedMode: item.message.visibleMode,
|
|
@@ -232,9 +424,58 @@ class ConversationKernel {
|
|
|
232
424
|
runId: item.message.runId,
|
|
233
425
|
createdAt: String(item.message.createdAt || ''),
|
|
234
426
|
images: item.message.images?.map(image => ({ ...image })),
|
|
427
|
+
branchNodeId: item.message.branchNodeId,
|
|
428
|
+
branchPath: item.message.branchPath?.slice(),
|
|
429
|
+
modelSelection: item.message.modelSelection,
|
|
430
|
+
waitingReason: waitingByBuildId.get(buildId),
|
|
431
|
+
blocked: waitingByBuildId.has(buildId),
|
|
235
432
|
}];
|
|
236
433
|
});
|
|
237
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Capture the exact runtime branch that owns a queued user turn. A queued
|
|
437
|
+
* Next is a new user turn, but it must still land on the branch the user was
|
|
438
|
+
* viewing when they enqueued it, not on whatever branch happens to be active
|
|
439
|
+
* when the previous Build settles.
|
|
440
|
+
*/
|
|
441
|
+
runtimeBranchIdentity(runtime) {
|
|
442
|
+
try {
|
|
443
|
+
const snapshot = runtime.runner.getConversationSnapshot(runtime.id);
|
|
444
|
+
const path = Array.isArray(snapshot.runtimeBranchNodePath)
|
|
445
|
+
? snapshot.runtimeBranchNodePath.map(id => String(id || '')).filter(Boolean)
|
|
446
|
+
: [];
|
|
447
|
+
const branchNodeId = String(snapshot.runtimeBranchId || snapshot.activeBranchId || path[path.length - 1] || '');
|
|
448
|
+
return {
|
|
449
|
+
branchNodeId,
|
|
450
|
+
branchPath: path.length ? path : (branchNodeId ? [branchNodeId] : []),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return { branchNodeId: '', branchPath: [] };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Map the current conversation/branch model onto the continuation-v1
|
|
459
|
+
* Root/Branch identity. The ledger is workspace-scoped and shared by the
|
|
460
|
+
* PC GUI, hosted mobile API and isolated runtimes.
|
|
461
|
+
*/
|
|
462
|
+
continuationStore(runtime, branchNodeIdOverride = '') {
|
|
463
|
+
const workspace = runtime.target.workspace;
|
|
464
|
+
if (!workspace?.path)
|
|
465
|
+
return null;
|
|
466
|
+
const workspaceId = String(workspace.id || workspace.path);
|
|
467
|
+
const branchIdentity = this.runtimeBranchIdentity(runtime);
|
|
468
|
+
const rootId = runtime.target.conversationId;
|
|
469
|
+
// Ledger branch ids are workspace-global, while a runtime branch node id
|
|
470
|
+
// is only unique inside its conversation/root. Namespace it by root so two
|
|
471
|
+
// conversations cannot share one authoritative branch record.
|
|
472
|
+
const branchId = branchNodeIdOverride.includes('::')
|
|
473
|
+
? branchNodeIdOverride
|
|
474
|
+
: `${rootId}::${branchNodeIdOverride || branchIdentity.branchNodeId || 'main'}`;
|
|
475
|
+
const store = new store_1.GuardedContinuationStore(workspace.path, workspaceId);
|
|
476
|
+
store.ensureRoot(rootId, branchId);
|
|
477
|
+
return { store, workspaceId, rootId, branchId };
|
|
478
|
+
}
|
|
238
479
|
enqueueNext(target, input) {
|
|
239
480
|
const runtime = this.findRuntime(target);
|
|
240
481
|
if (!runtime)
|
|
@@ -249,8 +490,56 @@ class ConversationKernel {
|
|
|
249
490
|
return existing;
|
|
250
491
|
const prompt = `[Next queued while current turn is running]\n${text}`;
|
|
251
492
|
const createdAt = String(input.createdAt || new Date().toISOString());
|
|
493
|
+
const capturedBranch = this.runtimeBranchIdentity(runtime);
|
|
494
|
+
const branchPath = Array.isArray(input.branchPath) && input.branchPath.length
|
|
495
|
+
? input.branchPath.map(id => String(id || '')).filter(Boolean)
|
|
496
|
+
: capturedBranch.branchPath;
|
|
497
|
+
const branchNodeId = String(input.branchNodeId || branchPath[branchPath.length - 1] || capturedBranch.branchNodeId || '');
|
|
498
|
+
const targetRuntimeKey = String(input.targetRuntimeKey || runtime.target.runtimeKey).trim() || runtime.target.runtimeKey;
|
|
499
|
+
const workspaceKey = runtime.target.workspaceKey;
|
|
500
|
+
const modelSelection = String(input.modelSelection || runtime.options.model || '').trim() || undefined;
|
|
501
|
+
// Gate B/C: admission is an authoritative CAS transaction. The in-memory
|
|
502
|
+
// pendingNextTurn list is only a projection of this durable build.
|
|
503
|
+
const continuation = this.continuationStore(runtime, branchNodeId);
|
|
504
|
+
const ledgerBranchId = continuation?.branchId;
|
|
505
|
+
let buildId = '';
|
|
506
|
+
if (continuation) {
|
|
507
|
+
// 入队严格保持 0.6.3 语义:新命令接在当前队尾之后。被失败前置阻断的行不会
|
|
508
|
+
// 被静默跳过;用户可通过界面上的“修复受阻队列”显式动作重建可执行链。
|
|
509
|
+
const authoritative = continuation.store.snapshot({
|
|
510
|
+
workspaceId: continuation.workspaceId,
|
|
511
|
+
rootId: continuation.rootId,
|
|
512
|
+
branchId: continuation.branchId,
|
|
513
|
+
});
|
|
514
|
+
const receipt = continuation.store.enqueueBuild({
|
|
515
|
+
schemaVersion: 1,
|
|
516
|
+
type: 'ENQUEUE_BUILD',
|
|
517
|
+
commandId: id,
|
|
518
|
+
actorId: 'kernel',
|
|
519
|
+
target: {
|
|
520
|
+
workspaceId: continuation.workspaceId,
|
|
521
|
+
expectedRootId: continuation.rootId,
|
|
522
|
+
branchId: continuation.branchId,
|
|
523
|
+
},
|
|
524
|
+
expectedTailBuildId: authoritative.tailBuildId,
|
|
525
|
+
expectedQueueRevision: authoritative.queueRevision,
|
|
526
|
+
input: {
|
|
527
|
+
text,
|
|
528
|
+
images: input.images,
|
|
529
|
+
requestedMode: input.requestedMode,
|
|
530
|
+
goalObjective: input.goalObjective,
|
|
531
|
+
modelSelection,
|
|
532
|
+
branchNodeId: branchNodeId || undefined,
|
|
533
|
+
branchPath: branchPath.length ? branchPath : undefined,
|
|
534
|
+
},
|
|
535
|
+
recipeSnapshotId: `${continuation.rootId}:${continuation.branchId}:${modelSelection || 'auto'}`,
|
|
536
|
+
createdAt,
|
|
537
|
+
});
|
|
538
|
+
buildId = receipt.buildId;
|
|
539
|
+
}
|
|
252
540
|
runtime.pendingNextTurn.push({
|
|
253
541
|
message: {
|
|
542
|
+
buildId: buildId || undefined,
|
|
254
543
|
text: prompt,
|
|
255
544
|
visibleUserInput: text,
|
|
256
545
|
visibleMode: String(input.requestedMode || 'build'),
|
|
@@ -258,11 +547,18 @@ class ConversationKernel {
|
|
|
258
547
|
images: Array.isArray(input.images) ? input.images.slice(0, 6).map(image => ({ ...image })) : undefined,
|
|
259
548
|
clientMessageId: id,
|
|
260
549
|
runId: runtime.runId,
|
|
550
|
+
branchNodeId: branchNodeId || undefined,
|
|
551
|
+
branchPath: branchPath.length ? branchPath : undefined,
|
|
552
|
+
ledgerBranchId,
|
|
553
|
+
targetRuntimeKey,
|
|
554
|
+
workspaceKey,
|
|
555
|
+
modelSelection,
|
|
261
556
|
createdAt,
|
|
262
557
|
},
|
|
263
558
|
queueMode: 'followUp',
|
|
264
559
|
});
|
|
265
560
|
runtime.runner.retainConversationContinuations([{
|
|
561
|
+
buildId: buildId || undefined,
|
|
266
562
|
content: prompt,
|
|
267
563
|
queueMode: 'followUp',
|
|
268
564
|
clientMessageId: id,
|
|
@@ -272,6 +568,12 @@ class ConversationKernel {
|
|
|
272
568
|
visibleUserInput: text,
|
|
273
569
|
visibleMode: String(input.requestedMode || 'build'),
|
|
274
570
|
goalObjective: input.goalObjective,
|
|
571
|
+
branchNodeId: branchNodeId || undefined,
|
|
572
|
+
branchPath: branchPath.length ? branchPath : undefined,
|
|
573
|
+
ledgerBranchId,
|
|
574
|
+
targetRuntimeKey,
|
|
575
|
+
workspaceKey,
|
|
576
|
+
modelSelection,
|
|
275
577
|
}]);
|
|
276
578
|
this.trackQueuedMessage(runtime, prompt, 'followUp');
|
|
277
579
|
this.emitQueueUpdate(runtime);
|
|
@@ -291,14 +593,35 @@ class ConversationKernel {
|
|
|
291
593
|
if (!pending || typeof pending.message === 'string')
|
|
292
594
|
throw new Error('Queue item is no longer editable');
|
|
293
595
|
const previous = runtime.runner.conversationContinuations().find(item => item.clientMessageId === id);
|
|
294
|
-
|
|
596
|
+
const nextText = `[Next queued while current turn is running]\n${text}`;
|
|
597
|
+
const nextVisibleMode = input.requestedMode !== undefined ? input.requestedMode : pending.message.visibleMode;
|
|
598
|
+
const nextGoalObjective = input.goalObjective !== undefined ? input.goalObjective : pending.message.goalObjective;
|
|
599
|
+
const nextImages = input.images !== undefined
|
|
600
|
+
? input.images.map(image => ({ ...image }))
|
|
601
|
+
: pending.message.images?.map(image => ({ ...image }));
|
|
602
|
+
const continuation = this.continuationStore(runtime, pending.message.ledgerBranchId || pending.message.branchNodeId);
|
|
603
|
+
if (continuation && pending.message.buildId) {
|
|
604
|
+
continuation.store.updateQueuedBuild({
|
|
605
|
+
workspaceId: continuation.workspaceId,
|
|
606
|
+
rootId: continuation.rootId,
|
|
607
|
+
branchId: continuation.branchId,
|
|
608
|
+
buildId: pending.message.buildId,
|
|
609
|
+
input: {
|
|
610
|
+
text,
|
|
611
|
+
images: nextImages,
|
|
612
|
+
requestedMode: nextVisibleMode,
|
|
613
|
+
goalObjective: nextGoalObjective,
|
|
614
|
+
modelSelection: pending.message.modelSelection,
|
|
615
|
+
branchNodeId: pending.message.branchNodeId,
|
|
616
|
+
branchPath: pending.message.branchPath,
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
pending.message.text = nextText;
|
|
295
621
|
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 }));
|
|
622
|
+
pending.message.visibleMode = nextVisibleMode;
|
|
623
|
+
pending.message.goalObjective = nextGoalObjective;
|
|
624
|
+
pending.message.images = nextImages;
|
|
302
625
|
runtime.runner.retainConversationContinuations([{
|
|
303
626
|
...previous,
|
|
304
627
|
content: pending.message.text,
|
|
@@ -309,6 +632,11 @@ class ConversationKernel {
|
|
|
309
632
|
visibleUserInput: text,
|
|
310
633
|
visibleMode: pending.message.visibleMode,
|
|
311
634
|
goalObjective: pending.message.goalObjective,
|
|
635
|
+
branchNodeId: pending.message.branchNodeId,
|
|
636
|
+
branchPath: pending.message.branchPath,
|
|
637
|
+
targetRuntimeKey: pending.message.targetRuntimeKey,
|
|
638
|
+
workspaceKey: pending.message.workspaceKey,
|
|
639
|
+
modelSelection: pending.message.modelSelection,
|
|
312
640
|
}]);
|
|
313
641
|
this.emitQueueUpdate(runtime);
|
|
314
642
|
return this.queueItems(runtime.target).find(item => item.id === id);
|
|
@@ -321,7 +649,19 @@ class ConversationKernel {
|
|
|
321
649
|
const index = runtime.pendingNextTurn.findIndex(item => typeof item.message !== 'string' && item.message.clientMessageId === id);
|
|
322
650
|
if (index < 0)
|
|
323
651
|
return false;
|
|
324
|
-
const
|
|
652
|
+
const removed = runtime.pendingNextTurn[index];
|
|
653
|
+
if (typeof removed.message !== 'string') {
|
|
654
|
+
const continuation = this.continuationStore(runtime, removed.message.ledgerBranchId || removed.message.branchNodeId);
|
|
655
|
+
if (continuation && removed.message.buildId) {
|
|
656
|
+
continuation.store.deleteQueuedBuild({
|
|
657
|
+
workspaceId: continuation.workspaceId,
|
|
658
|
+
rootId: continuation.rootId,
|
|
659
|
+
branchId: continuation.branchId,
|
|
660
|
+
buildId: removed.message.buildId,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
runtime.pendingNextTurn.splice(index, 1);
|
|
325
665
|
if (typeof removed.message !== 'string') {
|
|
326
666
|
runtime.runner.consumeConversationContinuation({ content: removed.message.text, queueMode: removed.queueMode, clientMessageId: id });
|
|
327
667
|
}
|
|
@@ -355,6 +695,27 @@ class ConversationKernel {
|
|
|
355
695
|
return [];
|
|
356
696
|
return [[String(item.message.clientMessageId), item]];
|
|
357
697
|
}));
|
|
698
|
+
const firstOrdered = pendingById.get(orderedIds[0] || '');
|
|
699
|
+
const continuation = this.continuationStore(runtime, firstOrdered && typeof firstOrdered.message !== 'string'
|
|
700
|
+
? (firstOrdered.message.ledgerBranchId || firstOrdered.message.branchNodeId || '')
|
|
701
|
+
: '');
|
|
702
|
+
if (continuation) {
|
|
703
|
+
const orderedBuildIds = orderedIds
|
|
704
|
+
.map(id => {
|
|
705
|
+
const item = pendingById.get(id);
|
|
706
|
+
return item && typeof item.message !== 'string' ? item.message.buildId : undefined;
|
|
707
|
+
})
|
|
708
|
+
.filter((id) => !!id);
|
|
709
|
+
if (orderedBuildIds.length !== orderedIds.length) {
|
|
710
|
+
throw new Error('A queued build id is missing from the authoritative ledger projection');
|
|
711
|
+
}
|
|
712
|
+
continuation.store.reorderQueuedBuilds({
|
|
713
|
+
workspaceId: continuation.workspaceId,
|
|
714
|
+
rootId: continuation.rootId,
|
|
715
|
+
branchId: continuation.branchId,
|
|
716
|
+
orderedBuildIds,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
358
719
|
const reorderedPending = orderedIds.map(id => pendingById.get(id));
|
|
359
720
|
let nextIndex = 0;
|
|
360
721
|
runtime.pendingNextTurn = runtime.pendingNextTurn.map(item => {
|
|
@@ -369,6 +730,10 @@ class ConversationKernel {
|
|
|
369
730
|
return this.queueItems(runtime.target);
|
|
370
731
|
}
|
|
371
732
|
setQueuePaused(target, paused) {
|
|
733
|
+
// 队列策略只作用于已经存在的 runtime:在这里按需重建 runtime 会让空闲会话
|
|
734
|
+
// 重新变成“持有排队工作”的所有者,破坏工作区清理契约
|
|
735
|
+
// (`Cannot release a conversation with running or queued work`)。界面路径统一
|
|
736
|
+
// 走 queueAction,它已经负责按需创建 runtime。
|
|
372
737
|
const runtime = this.findRuntime(target);
|
|
373
738
|
if (!runtime)
|
|
374
739
|
throw new Error('Target conversation runtime is unavailable');
|
|
@@ -381,6 +746,35 @@ class ConversationKernel {
|
|
|
381
746
|
}
|
|
382
747
|
return runtime.queuePaused;
|
|
383
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* dev-0.6.4 hotfix: 显式修复被失败前置阻断的排队行。
|
|
751
|
+
*
|
|
752
|
+
* 0.6.3 的合约是「失败/取消阻断后继,且不静默跳过」,所以这里不挂在恢复动作上
|
|
753
|
+
* 自动执行;它必须由用户的显式动作触发(界面上的“修复受阻队列”按钮)。每一次
|
|
754
|
+
* 改挂都写入 `BuildQueueRepaired` 事件并发布可见状态,触发者与原因可审计。
|
|
755
|
+
*/
|
|
756
|
+
repairBlockedQueueNow(runtime) {
|
|
757
|
+
const continuation = this.continuationStore(runtime);
|
|
758
|
+
if (!continuation)
|
|
759
|
+
return 0;
|
|
760
|
+
try {
|
|
761
|
+
const result = continuation.store.repairBlockedQueue({
|
|
762
|
+
workspaceId: continuation.workspaceId,
|
|
763
|
+
rootId: continuation.rootId,
|
|
764
|
+
branchId: continuation.branchId,
|
|
765
|
+
reason: 'explicit-user-repair',
|
|
766
|
+
});
|
|
767
|
+
if (result.repaired.length) {
|
|
768
|
+
runtime.runner.recordWorkStatus(`${result.repaired.length} queued row(s) blocked by a failed predecessor were re-anchored to the last committed build; the queue resumes in order.`);
|
|
769
|
+
this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
|
|
770
|
+
}
|
|
771
|
+
return result.repaired.length;
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
runtime.runner.recordWorkStatus(`Queue repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
775
|
+
return 0;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
384
778
|
queueAction(target, action, input = {}) {
|
|
385
779
|
const normalized = this.normalizeTarget(target);
|
|
386
780
|
const runtime = this.findRuntime(normalized) || this.runtime(normalized, {
|
|
@@ -397,6 +791,10 @@ class ConversationKernel {
|
|
|
397
791
|
goalObjective: input.goalObjective,
|
|
398
792
|
createdAt: input.createdAt,
|
|
399
793
|
images: input.images,
|
|
794
|
+
branchNodeId: input.branchNodeId,
|
|
795
|
+
branchPath: input.branchPath,
|
|
796
|
+
modelSelection: input.modelSelection,
|
|
797
|
+
targetRuntimeKey: input.targetRuntimeKey,
|
|
400
798
|
});
|
|
401
799
|
}
|
|
402
800
|
else if (action === 'update') {
|
|
@@ -415,6 +813,10 @@ class ConversationKernel {
|
|
|
415
813
|
else if (action === 'set_pause') {
|
|
416
814
|
this.setQueuePaused(runtime.target, input.paused === true);
|
|
417
815
|
}
|
|
816
|
+
else if (action === 'repair_blocked') {
|
|
817
|
+
// 显式修复:把被失败前置阻断的排队行重新挂到最后一个已提交 Build。
|
|
818
|
+
this.repairBlockedQueueNow(runtime);
|
|
819
|
+
}
|
|
418
820
|
else if (action === 'guide') {
|
|
419
821
|
if (input.text !== undefined)
|
|
420
822
|
this.updateQueueItem(runtime.target, String(input.id || ''), input.text, input);
|
|
@@ -948,7 +1350,7 @@ class ConversationKernel {
|
|
|
948
1350
|
// carry no send-time model/mode; the running block keeps its settings and
|
|
949
1351
|
// the next dequeue follows the current conversation selection (which
|
|
950
1352
|
// setModel/setMode already recorded on runtime.options).
|
|
951
|
-
this.enqueueSameSession(active, message, queueMode);
|
|
1353
|
+
this.enqueueSameSession(active, message, queueMode, options.model);
|
|
952
1354
|
this.activateAcceptedGoal(active, typeof message === 'string' ? '' : message.goalObjective);
|
|
953
1355
|
return active.activePromise;
|
|
954
1356
|
}
|
|
@@ -1071,11 +1473,17 @@ class ConversationKernel {
|
|
|
1071
1473
|
return this.result(runtime, lastTokens);
|
|
1072
1474
|
}
|
|
1073
1475
|
for (;;) {
|
|
1074
|
-
while (!runtime.queuePaused
|
|
1476
|
+
while (!runtime.queuePaused) {
|
|
1075
1477
|
if (runtime.stopRequestedRunId === runtime.runId)
|
|
1076
1478
|
return this.result(runtime, lastTokens);
|
|
1077
|
-
|
|
1078
|
-
|
|
1479
|
+
// Only intervening Guides run inside the current Build. A Next item
|
|
1480
|
+
// is a new user turn and stays exactly where the user placed it; the
|
|
1481
|
+
// run finalizer starts a fresh Build for it after this one settles.
|
|
1482
|
+
const steerIndex = runtime.pendingNextTurn.findIndex(item => item.queueMode === 'steer');
|
|
1483
|
+
if (steerIndex < 0)
|
|
1484
|
+
break;
|
|
1485
|
+
const next = runtime.pendingNextTurn.splice(steerIndex, 1)[0];
|
|
1486
|
+
if (typeof next.message !== 'string' && !!next.message.clientMessageId) {
|
|
1079
1487
|
const batchGuides = [];
|
|
1080
1488
|
const pushGuide = (message) => {
|
|
1081
1489
|
batchGuides.push({
|
|
@@ -1087,11 +1495,13 @@ class ConversationKernel {
|
|
|
1087
1495
|
});
|
|
1088
1496
|
};
|
|
1089
1497
|
pushGuide(next.message);
|
|
1090
|
-
while (
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1498
|
+
while (true) {
|
|
1499
|
+
const guideIndex = runtime.pendingNextTurn.findIndex(item => item.queueMode === 'steer'
|
|
1500
|
+
&& typeof item.message !== 'string'
|
|
1501
|
+
&& !!item.message.clientMessageId);
|
|
1502
|
+
if (guideIndex < 0)
|
|
1503
|
+
break;
|
|
1504
|
+
const guide = runtime.pendingNextTurn.splice(guideIndex, 1)[0];
|
|
1095
1505
|
pushGuide(guide.message);
|
|
1096
1506
|
}
|
|
1097
1507
|
if (batchGuides.length === 1) {
|
|
@@ -1111,17 +1521,10 @@ class ConversationKernel {
|
|
|
1111
1521
|
break;
|
|
1112
1522
|
}
|
|
1113
1523
|
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))
|
|
1524
|
+
lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
|
|
1525
|
+
if (this.repeatedAutomaticAssistant(runtime, next.message, next.queueMode))
|
|
1124
1526
|
break;
|
|
1527
|
+
continue;
|
|
1125
1528
|
}
|
|
1126
1529
|
}
|
|
1127
1530
|
const rootMessage = runtime.runner.subagents.readRootInbox()[0];
|
|
@@ -1144,7 +1547,7 @@ class ConversationKernel {
|
|
|
1144
1547
|
if (runtime.runId === completedRunId
|
|
1145
1548
|
&& runtime.stopRequestedRunId !== completedRunId
|
|
1146
1549
|
&& !runtime.queuePaused
|
|
1147
|
-
&& runtime.pendingNextTurn.
|
|
1550
|
+
&& runtime.pendingNextTurn.some(item => item.queueMode === 'steer')) {
|
|
1148
1551
|
runtime.guideAcceptanceClosedRunId = '';
|
|
1149
1552
|
if (!runtime.runner.resumeConversationWorkRun(completedRunId)) {
|
|
1150
1553
|
throw new Error(`Unable to resume deferred Guide work run ${completedRunId}`);
|
|
@@ -1331,6 +1734,9 @@ class ConversationKernel {
|
|
|
1331
1734
|
&& runtime.queued.followUp.length === 0
|
|
1332
1735
|
&& runner.subagents.readRootInbox().length === 0;
|
|
1333
1736
|
});
|
|
1737
|
+
// 执行投影严格跟随当前激活分支节点:新分支绝不接管另一条分页的排队输入
|
|
1738
|
+
// (dev-0.6.3 的既有契约)。界面上跨分页的排队行可见性由渲染层负责,
|
|
1739
|
+
// 执行始终属于受理时捕获的那条分支。
|
|
1334
1740
|
for (const continuation of runner.conversationContinuations()) {
|
|
1335
1741
|
runtime.pendingNextTurn.push({
|
|
1336
1742
|
message: continuation.clientMessageId || continuation.images?.length || continuation.hiddenUserInput
|
|
@@ -1343,6 +1749,12 @@ class ConversationKernel {
|
|
|
1343
1749
|
visibleUserInput: continuation.visibleUserInput,
|
|
1344
1750
|
visibleMode: continuation.visibleMode,
|
|
1345
1751
|
goalObjective: continuation.goalObjective,
|
|
1752
|
+
buildId: continuation.buildId,
|
|
1753
|
+
modelSelection: continuation.modelSelection,
|
|
1754
|
+
branchNodeId: continuation.branchNodeId,
|
|
1755
|
+
branchPath: continuation.branchPath?.slice(),
|
|
1756
|
+
targetRuntimeKey: continuation.targetRuntimeKey,
|
|
1757
|
+
workspaceKey: continuation.workspaceKey,
|
|
1346
1758
|
createdAt: continuation.createdAt,
|
|
1347
1759
|
hiddenUserInput: continuation.hiddenUserInput,
|
|
1348
1760
|
}
|
|
@@ -1454,13 +1866,110 @@ class ConversationKernel {
|
|
|
1454
1866
|
runtime.pendingContinuationRunId = undefined;
|
|
1455
1867
|
if (runtime.preparingArchive || runtime.runId !== runId || runtime.activePromise || runtime.externalOwner || runtime.stopRequestedRunId === runId || runtime.queuePaused)
|
|
1456
1868
|
return;
|
|
1457
|
-
const next = runtime.pendingNextTurn
|
|
1869
|
+
const next = runtime.pendingNextTurn[0];
|
|
1458
1870
|
if (!next)
|
|
1459
1871
|
return;
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1872
|
+
const queued = typeof next.message === 'string' ? null : next.message;
|
|
1873
|
+
if (queued?.targetRuntimeKey && queued.targetRuntimeKey !== runtime.target.runtimeKey) {
|
|
1874
|
+
runtime.queuePaused = true;
|
|
1875
|
+
runtime.runner.recordWorkStatus('Queued turn target mismatch; the queue is paused instead of running in another conversation.');
|
|
1876
|
+
this.emitQueueUpdate(runtime);
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
runtime.pendingNextTurn.shift();
|
|
1880
|
+
if (next.queueMode === 'followUp') {
|
|
1881
|
+
// A Next item is a fresh user turn. It must start a new Build in this
|
|
1882
|
+
// exact conversation after the previous one has fully settled, and it
|
|
1883
|
+
// must land on the branch that owned it when the user enqueued it.
|
|
1884
|
+
// Never carry the completed runId into the new turn.
|
|
1885
|
+
const delivery = this.drainQueuedFollowUpMessage(next.message);
|
|
1886
|
+
const message = typeof delivery === 'string'
|
|
1887
|
+
? { text: delivery }
|
|
1888
|
+
: { ...delivery, runId: undefined };
|
|
1889
|
+
const continuation = this.continuationStore(runtime);
|
|
1890
|
+
let scope = null;
|
|
1891
|
+
void this.runPendingQueueItem(runtime, next, async () => {
|
|
1892
|
+
this.bindQueuedTurnBranch(runtime, queued);
|
|
1893
|
+
if (queued?.modelSelection)
|
|
1894
|
+
runtime.options.model = queued.modelSelection;
|
|
1895
|
+
if (message.visibleMode && ['build', 'chat', 'plan', 'goal'].includes(message.visibleMode)) {
|
|
1896
|
+
this.setMode(runtime.target, message.visibleMode);
|
|
1897
|
+
}
|
|
1898
|
+
this.activateAcceptedGoal(runtime, message.goalObjective || '');
|
|
1899
|
+
if (continuation && queued?.buildId) {
|
|
1900
|
+
const authoritative = continuation.store.snapshot({
|
|
1901
|
+
workspaceId: continuation.workspaceId,
|
|
1902
|
+
rootId: continuation.rootId,
|
|
1903
|
+
branchId: continuation.branchId,
|
|
1904
|
+
});
|
|
1905
|
+
const ready = authoritative.builds
|
|
1906
|
+
.filter(build => build.status === 'QUEUED')
|
|
1907
|
+
.sort((a, b) => a.queueSequence - b.queueSequence)[0];
|
|
1908
|
+
if (ready && ready.buildId !== queued.buildId) {
|
|
1909
|
+
// The in-memory projection is not the authoritative next build.
|
|
1910
|
+
// Keep this row queued instead of executing out of order.
|
|
1911
|
+
runtime.pendingNextTurn.unshift(next);
|
|
1912
|
+
runtime.runner.recordWorkStatus('Queued turn is waiting for the authoritative predecessor.');
|
|
1913
|
+
this.emitQueueUpdate(runtime);
|
|
1914
|
+
return this.result(runtime, []);
|
|
1915
|
+
}
|
|
1916
|
+
scope = continuation.store.claimBuild({
|
|
1917
|
+
workspaceId: continuation.workspaceId,
|
|
1918
|
+
rootId: continuation.rootId,
|
|
1919
|
+
branchId: continuation.branchId,
|
|
1920
|
+
workerCapability: `kernel:${process.pid}`,
|
|
1921
|
+
});
|
|
1922
|
+
if (scope && scope.buildId !== queued.buildId) {
|
|
1923
|
+
continuation.store.failBuild({ scope, reason: 'Authoritative build mismatch during claim' });
|
|
1924
|
+
scope = null;
|
|
1925
|
+
}
|
|
1926
|
+
if (!scope) {
|
|
1927
|
+
runtime.pendingNextTurn.unshift(next);
|
|
1928
|
+
runtime.runner.recordWorkStatus('Queued turn is not ready for execution yet.');
|
|
1929
|
+
this.emitQueueUpdate(runtime);
|
|
1930
|
+
return this.result(runtime, []);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
const result = await this.prompt(message, runtime.target, runtime.options, 'followUp');
|
|
1934
|
+
if (continuation && scope) {
|
|
1935
|
+
continuation.store.commitFinal({
|
|
1936
|
+
scope,
|
|
1937
|
+
text: result.tokens.map(token => token.text || '').join(''),
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
return result;
|
|
1941
|
+
}, (accepted, error) => {
|
|
1942
|
+
if (!continuation || !scope)
|
|
1943
|
+
return;
|
|
1944
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1945
|
+
if (accepted) {
|
|
1946
|
+
try {
|
|
1947
|
+
continuation.store.failBuild({ scope, reason });
|
|
1948
|
+
}
|
|
1949
|
+
catch { /* stop/archive may have already released the guard */ }
|
|
1950
|
+
}
|
|
1951
|
+
else {
|
|
1952
|
+
// The input was never accepted. Stop/archive may already have
|
|
1953
|
+
// released the guard, so use the idempotent requeue path: the same
|
|
1954
|
+
// durable build id stays available for an explicit resume.
|
|
1955
|
+
try {
|
|
1956
|
+
continuation.store.requeueBuild({
|
|
1957
|
+
workspaceId: continuation.workspaceId,
|
|
1958
|
+
rootId: continuation.rootId,
|
|
1959
|
+
branchId: continuation.branchId,
|
|
1960
|
+
buildId: scope.buildId,
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
catch { /* the build may already be committed or explicitly cancelled */ }
|
|
1964
|
+
}
|
|
1965
|
+
}).catch(() => {
|
|
1966
|
+
// Agent.process and the work-run finalizer already publish the error.
|
|
1967
|
+
});
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const message = typeof next.message === 'string'
|
|
1971
|
+
? { text: next.message, runId }
|
|
1972
|
+
: { ...next.message, runId: next.message.runId || runId };
|
|
1464
1973
|
void this.runPendingQueueItem(runtime, next, async () => {
|
|
1465
1974
|
if (message.visibleMode && ['build', 'chat', 'plan', 'goal'].includes(message.visibleMode)) {
|
|
1466
1975
|
this.setMode(runtime.target, message.visibleMode);
|
|
@@ -1471,7 +1980,27 @@ class ConversationKernel {
|
|
|
1471
1980
|
});
|
|
1472
1981
|
});
|
|
1473
1982
|
}
|
|
1474
|
-
|
|
1983
|
+
/**
|
|
1984
|
+
* Restore the runtime branch captured by a queued user turn. If the branch
|
|
1985
|
+
* disappeared (manual deletion/rewind), fail the queued item into the
|
|
1986
|
+
* existing pause/retry path instead of appending it to a different branch.
|
|
1987
|
+
*/
|
|
1988
|
+
bindQueuedTurnBranch(runtime, message) {
|
|
1989
|
+
const branchNodeId = String(message?.branchNodeId || '').trim();
|
|
1990
|
+
if (!branchNodeId)
|
|
1991
|
+
return;
|
|
1992
|
+
const snapshot = runtime.runner.getConversationSnapshot(runtime.id);
|
|
1993
|
+
const currentBranch = String(snapshot.runtimeBranchId || snapshot.activeBranchId || '');
|
|
1994
|
+
if (currentBranch === branchNodeId)
|
|
1995
|
+
return;
|
|
1996
|
+
try {
|
|
1997
|
+
runtime.runner.switchConversationBranch(runtime.id, branchNodeId);
|
|
1998
|
+
}
|
|
1999
|
+
catch (error) {
|
|
2000
|
+
throw new Error(`Queued turn branch ${branchNodeId} is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
async runPendingQueueItem(runtime, next, execute, onFailure) {
|
|
1475
2004
|
const queuedMessage = typeof next.message === 'string' ? undefined : next.message;
|
|
1476
2005
|
const id = queuedMessage?.clientMessageId;
|
|
1477
2006
|
if (next.queueMode !== 'followUp' || !id)
|
|
@@ -1491,6 +2020,10 @@ class ConversationKernel {
|
|
|
1491
2020
|
// the same row manageable. A provider failure after acceptance must
|
|
1492
2021
|
// never replay the already submitted user input after restart/resume.
|
|
1493
2022
|
runtime.queuePaused = true;
|
|
2023
|
+
try {
|
|
2024
|
+
onFailure?.(accepted, error);
|
|
2025
|
+
}
|
|
2026
|
+
catch { /* the original failure is authoritative */ }
|
|
1494
2027
|
throw error;
|
|
1495
2028
|
}
|
|
1496
2029
|
finally {
|
|
@@ -1669,13 +2202,26 @@ class ConversationKernel {
|
|
|
1669
2202
|
guides: (run.guides || []).map(bindGuide),
|
|
1670
2203
|
}));
|
|
1671
2204
|
}
|
|
1672
|
-
enqueueSameSession(runtime, message, queueMode) {
|
|
2205
|
+
enqueueSameSession(runtime, message, queueMode, requestedModel = '') {
|
|
1673
2206
|
const isSteer = queueMode === 'steer';
|
|
1674
2207
|
const text = typeof message === 'string' ? message : message.text;
|
|
1675
2208
|
const prompt = isSteer ? text : `[Next queued while current turn is running]\n${text}`;
|
|
1676
2209
|
if (!isSteer) {
|
|
1677
2210
|
const structured = typeof message === 'string' ? null : message;
|
|
2211
|
+
const requestedRuntimeKey = String(structured?.targetRuntimeKey || '').trim();
|
|
2212
|
+
if (requestedRuntimeKey && requestedRuntimeKey !== runtime.target.runtimeKey) {
|
|
2213
|
+
throw new Error(`Queued message target mismatch: expected ${requestedRuntimeKey}, received ${runtime.target.runtimeKey}`);
|
|
2214
|
+
}
|
|
1678
2215
|
const clientMessageId = String(structured?.clientMessageId || (0, crypto_1.randomUUID)());
|
|
2216
|
+
const capturedBranch = this.runtimeBranchIdentity(runtime);
|
|
2217
|
+
const branchPath = Array.isArray(structured?.branchPath) && structured.branchPath.length
|
|
2218
|
+
? structured.branchPath.map(id => String(id || '')).filter(Boolean)
|
|
2219
|
+
: capturedBranch.branchPath;
|
|
2220
|
+
const branchNodeId = String(structured?.branchNodeId || branchPath[branchPath.length - 1] || capturedBranch.branchNodeId || '');
|
|
2221
|
+
const targetRuntimeKey = runtime.target.runtimeKey;
|
|
2222
|
+
const workspaceKey = runtime.target.workspaceKey;
|
|
2223
|
+
const modelSelection = String((structured?.modelSelection) || requestedModel || runtime.options.model || '').trim() || undefined;
|
|
2224
|
+
const ledgerBranchId = this.continuationStore(runtime, branchNodeId)?.branchId;
|
|
1679
2225
|
const queuedMessage = {
|
|
1680
2226
|
...(structured || {}),
|
|
1681
2227
|
text: prompt,
|
|
@@ -1683,6 +2229,12 @@ class ConversationKernel {
|
|
|
1683
2229
|
visibleMode: structured?.visibleMode || runtime.options.mode,
|
|
1684
2230
|
clientMessageId,
|
|
1685
2231
|
runId: structured?.runId || runtime.runId,
|
|
2232
|
+
branchNodeId: branchNodeId || undefined,
|
|
2233
|
+
branchPath: branchPath.length ? branchPath : undefined,
|
|
2234
|
+
ledgerBranchId,
|
|
2235
|
+
targetRuntimeKey,
|
|
2236
|
+
workspaceKey,
|
|
2237
|
+
modelSelection,
|
|
1686
2238
|
createdAt: String(structured?.createdAt || new Date().toISOString()),
|
|
1687
2239
|
};
|
|
1688
2240
|
if (!runtime.pendingNextTurn.some(item => typeof item.message !== 'string' && item.message.clientMessageId === clientMessageId)) {
|
|
@@ -1699,6 +2251,12 @@ class ConversationKernel {
|
|
|
1699
2251
|
visibleUserInput: queuedMessage.visibleUserInput,
|
|
1700
2252
|
visibleMode: queuedMessage.visibleMode,
|
|
1701
2253
|
goalObjective: queuedMessage.goalObjective,
|
|
2254
|
+
branchNodeId: queuedMessage.branchNodeId,
|
|
2255
|
+
branchPath: queuedMessage.branchPath,
|
|
2256
|
+
ledgerBranchId: queuedMessage.ledgerBranchId,
|
|
2257
|
+
targetRuntimeKey,
|
|
2258
|
+
workspaceKey,
|
|
2259
|
+
modelSelection,
|
|
1702
2260
|
}]);
|
|
1703
2261
|
this.trackQueuedMessage(runtime, prompt, 'followUp');
|
|
1704
2262
|
runtime.runner.recordWorkStatus(runtime.stopRequestedRunId === runtime.runId
|
|
@@ -1799,6 +2357,60 @@ class ConversationKernel {
|
|
|
1799
2357
|
.map(item => typeof item.message === 'string' ? item.message : item.message.text);
|
|
1800
2358
|
return runtime;
|
|
1801
2359
|
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Merge queue continuations persisted by another owner/kernel into this
|
|
2362
|
+
* runtime's in-memory projection. Flow takeover, cold restore and a pool
|
|
2363
|
+
* hand-off must not lose an already-admitted Next row. Deduplication is by
|
|
2364
|
+
* the stable clientMessageId, never by text.
|
|
2365
|
+
*/
|
|
2366
|
+
reconcilePendingContinuations(runtime) {
|
|
2367
|
+
// The other owner (pool/flow/cold restore) commits queue continuations to
|
|
2368
|
+
// the workspace state file. Reload this runner's durable view before
|
|
2369
|
+
// merging so a takeover cannot miss a row persisted after the runner was
|
|
2370
|
+
// first created.
|
|
2371
|
+
runtime.runner.setConversationFromStorage(runtime.id);
|
|
2372
|
+
const persisted = runtime.runner.conversationContinuations();
|
|
2373
|
+
const existingIds = new Set();
|
|
2374
|
+
for (const item of runtime.pendingNextTurn) {
|
|
2375
|
+
if (typeof item.message === 'string')
|
|
2376
|
+
continue;
|
|
2377
|
+
const id = String(item.message.clientMessageId || '').trim();
|
|
2378
|
+
if (id)
|
|
2379
|
+
existingIds.add(id);
|
|
2380
|
+
}
|
|
2381
|
+
let changed = false;
|
|
2382
|
+
for (const continuation of persisted) {
|
|
2383
|
+
if (continuation.queueMode !== 'followUp' || !continuation.clientMessageId)
|
|
2384
|
+
continue;
|
|
2385
|
+
if (existingIds.has(continuation.clientMessageId))
|
|
2386
|
+
continue;
|
|
2387
|
+
runtime.pendingNextTurn.push({
|
|
2388
|
+
message: {
|
|
2389
|
+
text: continuation.content,
|
|
2390
|
+
visibleUserInput: continuation.visibleUserInput,
|
|
2391
|
+
visibleMode: continuation.visibleMode,
|
|
2392
|
+
goalObjective: continuation.goalObjective,
|
|
2393
|
+
images: continuation.images?.map(image => ({ ...image })),
|
|
2394
|
+
attachments: continuation.attachments?.map(attachment => ({ ...attachment })),
|
|
2395
|
+
clientMessageId: continuation.clientMessageId,
|
|
2396
|
+
buildId: continuation.buildId,
|
|
2397
|
+
branchNodeId: continuation.branchNodeId,
|
|
2398
|
+
branchPath: continuation.branchPath?.slice(),
|
|
2399
|
+
targetRuntimeKey: continuation.targetRuntimeKey,
|
|
2400
|
+
workspaceKey: continuation.workspaceKey,
|
|
2401
|
+
modelSelection: continuation.modelSelection,
|
|
2402
|
+
createdAt: continuation.createdAt,
|
|
2403
|
+
},
|
|
2404
|
+
queueMode: 'followUp',
|
|
2405
|
+
});
|
|
2406
|
+
existingIds.add(continuation.clientMessageId);
|
|
2407
|
+
changed = true;
|
|
2408
|
+
}
|
|
2409
|
+
if (changed) {
|
|
2410
|
+
this.queueState(runtime);
|
|
2411
|
+
this.emitQueueUpdate(runtime);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
1802
2414
|
normalizeTarget(input) {
|
|
1803
2415
|
if (typeof input !== 'string') {
|
|
1804
2416
|
const withWorkspace = input.workspace ? input : { ...input, workspace: this.workspaceForId(input.workspaceId) };
|