@yeaft/webchat-agent 1.0.641 → 1.0.643
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/connection/message-router.js +4 -1
- package/index.js +1 -1
- package/local-runtime/server/handlers/agent-output.js +7 -5
- package/local-runtime/server/handlers/client-conversation.js +47 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +191 -133
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/conversation/persist.js +272 -24
- package/yeaft/conversation/session-history.js +336 -0
- package/yeaft/engine.js +28 -18
- package/yeaft/routing/router.js +4 -0
- package/yeaft/sessions/index.js +1 -0
- package/yeaft/sessions/session-crud.js +68 -3
- package/yeaft/tools/agent.js +4 -1
- package/yeaft/web-bridge.js +255 -54
package/yeaft/web-bridge.js
CHANGED
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
SessionCrudError,
|
|
83
83
|
createSessionFromSpec,
|
|
84
84
|
copySession,
|
|
85
|
+
mergeSessions,
|
|
85
86
|
renameSession,
|
|
86
87
|
updateSessionAnnouncement,
|
|
87
88
|
updateSessionWorkDir,
|
|
@@ -498,6 +499,90 @@ const vpEngineConfigKeys = new Map();
|
|
|
498
499
|
/** @type {Map<string, AbortController>} */
|
|
499
500
|
const vpAborts = new Map();
|
|
500
501
|
|
|
502
|
+
/**
|
|
503
|
+
* A conservative Session-wide activity epoch. Its durable prefix is captured
|
|
504
|
+
* synchronously before ingress can await boot/classification or persist a user
|
|
505
|
+
* row. Overlapping inputs, fan-out, handoffs and task reentry share that prefix;
|
|
506
|
+
* no active-turn boundary is reconstructed from the latest transcript row.
|
|
507
|
+
*/
|
|
508
|
+
const sessionCopyEpochs = new Map();
|
|
509
|
+
const modelReentryActivity = new Map();
|
|
510
|
+
|
|
511
|
+
function hasSessionRuntimeWork(sessionId) {
|
|
512
|
+
const prefix = `${sessionId}::`;
|
|
513
|
+
if ([...vpDrivers.keys(), ...vpAborts.keys()].some(key => key.startsWith(prefix))) return true;
|
|
514
|
+
for (const [key, inbox] of vpInboxes) {
|
|
515
|
+
if (key.startsWith(prefix) && inbox?.length) return true;
|
|
516
|
+
}
|
|
517
|
+
for (const [key, threads] of vpThreads) {
|
|
518
|
+
if (key.startsWith(prefix) && [...threads.values()].some(thread => thread.pendingQueries?.length)) return true;
|
|
519
|
+
}
|
|
520
|
+
for (const engine of asyncTaskOwners.values()) {
|
|
521
|
+
if (engine?.sessionId === sessionId) return true;
|
|
522
|
+
}
|
|
523
|
+
for (const task of session?.taskManager?.listActiveTasks?.(sessionId) || []) {
|
|
524
|
+
if (task.ownerVpId && taskResultDeliveryFor(task) === TASK_RESULT_DELIVERY.MODEL_REENTRY) return true;
|
|
525
|
+
}
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function settleSessionCopyEpoch(sessionId) {
|
|
530
|
+
const epoch = sessionCopyEpochs.get(sessionId);
|
|
531
|
+
if (epoch && epoch.pending === 0 && !hasSessionRuntimeWork(sessionId)) sessionCopyEpochs.delete(sessionId);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function beginSessionActivity(sessionId, { unknownBoundary = false } = {}) {
|
|
535
|
+
settleSessionCopyEpoch(sessionId);
|
|
536
|
+
let epoch = sessionCopyEpochs.get(sessionId);
|
|
537
|
+
if (!epoch) {
|
|
538
|
+
let boundary = null;
|
|
539
|
+
// Legacy/untracked running work has no trustworthy pre-ingress boundary.
|
|
540
|
+
if (!unknownBoundary && !hasSessionRuntimeWork(sessionId)) {
|
|
541
|
+
try {
|
|
542
|
+
const root = resolveSessionYeaftDir(ctx.CONFIG?.yeaftDir, sessionId);
|
|
543
|
+
const store = new ConversationStore(root);
|
|
544
|
+
boundary = JSON.stringify(store.captureSessionCopyBoundary(sessionId));
|
|
545
|
+
} catch { /* fail closed for running copies if capture was unavailable */ }
|
|
546
|
+
}
|
|
547
|
+
epoch = { boundary, pending: 0 };
|
|
548
|
+
sessionCopyEpochs.set(sessionId, epoch);
|
|
549
|
+
}
|
|
550
|
+
epoch.pending += 1;
|
|
551
|
+
let released = false;
|
|
552
|
+
return () => {
|
|
553
|
+
if (released) return;
|
|
554
|
+
released = true;
|
|
555
|
+
epoch.pending -= 1;
|
|
556
|
+
if (sessionCopyEpochs.get(sessionId) === epoch) settleSessionCopyEpoch(sessionId);
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function sessionHasActiveWork(sessionId) {
|
|
561
|
+
settleSessionCopyEpoch(sessionId);
|
|
562
|
+
return sessionCopyEpochs.has(sessionId) || hasSessionRuntimeWork(sessionId);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function trackModelReentryActivity(event) {
|
|
566
|
+
const task = event?.task;
|
|
567
|
+
if (!task?.sessionId || !task?.ownerVpId || taskResultDeliveryFor(task) !== TASK_RESULT_DELIVERY.MODEL_REENTRY) return null;
|
|
568
|
+
const key = `${task.sessionId}::${task.id}`;
|
|
569
|
+
let release = modelReentryActivity.get(key);
|
|
570
|
+
// Sub-agent final logging refreshes terminal tasks after completion. Such
|
|
571
|
+
// updates must never reacquire a lease: there is no later completion to free
|
|
572
|
+
// it. Still release an existing lease for any terminal-state notification.
|
|
573
|
+
if (isTerminalTaskStatus(task.status) && event.event !== 'completed') {
|
|
574
|
+
modelReentryActivity.delete(key);
|
|
575
|
+
return release || null;
|
|
576
|
+
}
|
|
577
|
+
if (!release) {
|
|
578
|
+
release = beginSessionActivity(task.sessionId, { unknownBoundary: event.event !== 'started' });
|
|
579
|
+
modelReentryActivity.set(key, release);
|
|
580
|
+
}
|
|
581
|
+
if (event.event !== 'completed') return null;
|
|
582
|
+
modelReentryActivity.delete(key);
|
|
583
|
+
return release;
|
|
584
|
+
}
|
|
585
|
+
|
|
501
586
|
/**
|
|
502
587
|
* Owner index for background tasks currently parked on a running engine.
|
|
503
588
|
* Populated by the per-engine async-task coordinator at register time;
|
|
@@ -594,32 +679,51 @@ function buildAsyncTaskCoordinator() {
|
|
|
594
679
|
deleteOwnerIfMatch(taskId, engine);
|
|
595
680
|
},
|
|
596
681
|
onUndelivered(taskId, delivery, engine) {
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
682
|
+
if (asyncTaskOwners.get(taskId) !== engine) return;
|
|
683
|
+
const sessionId = delivery?.sessionId || engine?.sessionId || null;
|
|
684
|
+
// Bridge ownership to the rescue before deleting the last live owner.
|
|
685
|
+
const releaseActivity = beginSessionActivity(sessionId);
|
|
686
|
+
try {
|
|
687
|
+
if (!deleteOwnerIfMatch(taskId, engine)) return;
|
|
688
|
+
let causalRootId = delivery?.causalRootId || null;
|
|
689
|
+
if (!causalRootId && sessionId) {
|
|
690
|
+
try { causalRootId = session?.taskManager?.getTask?.(sessionId, taskId)?.source?.causalRootId || null; } catch { /* legacy fallback is best-effort */ }
|
|
691
|
+
}
|
|
692
|
+
scheduleTaskResultRescue({
|
|
693
|
+
taskId,
|
|
694
|
+
sessionId,
|
|
695
|
+
vpId: delivery?.vpId || engine?.vpId || null,
|
|
696
|
+
threadId: delivery?.threadId || engine?.currentThreadId || 'main',
|
|
697
|
+
content: delivery?.content,
|
|
698
|
+
taskKind: delivery?.taskKind,
|
|
699
|
+
taskStatus: delivery?.taskStatus,
|
|
700
|
+
causalRootId,
|
|
701
|
+
});
|
|
702
|
+
} finally {
|
|
703
|
+
releaseActivity();
|
|
704
|
+
}
|
|
607
705
|
},
|
|
608
706
|
onDeferred(taskId, engine) {
|
|
609
|
-
if (!engine?.ownsPendingAsyncTask?.(taskId)) return;
|
|
610
|
-
if (!deleteOwnerIfMatch(taskId, engine)) return;
|
|
707
|
+
if (!engine?.ownsPendingAsyncTask?.(taskId) || asyncTaskOwners.get(taskId) !== engine) return;
|
|
611
708
|
const sessionId = engine?.sessionId || null;
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
taskId
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
709
|
+
const releaseActivity = beginSessionActivity(sessionId);
|
|
710
|
+
try {
|
|
711
|
+
if (!deleteOwnerIfMatch(taskId, engine)) return;
|
|
712
|
+
const task = sessionId && session?.taskManager?.getTask?.(sessionId, taskId);
|
|
713
|
+
if (!task || !isTerminalTaskStatus(task.status)) return;
|
|
714
|
+
scheduleTaskResultRescue({
|
|
715
|
+
taskId,
|
|
716
|
+
sessionId: task.sessionId || sessionId,
|
|
717
|
+
vpId: task.ownerVpId || engine?.vpId || null,
|
|
718
|
+
threadId: task.source?.threadId || task.runtime?.threadId || engine?.currentThreadId || 'main',
|
|
719
|
+
content: formatTaskResultForVp(task),
|
|
720
|
+
taskKind: task.kind,
|
|
721
|
+
taskStatus: task.status,
|
|
722
|
+
causalRootId: task.source?.causalRootId || null,
|
|
723
|
+
});
|
|
724
|
+
} finally {
|
|
725
|
+
releaseActivity();
|
|
726
|
+
}
|
|
623
727
|
},
|
|
624
728
|
};
|
|
625
729
|
}
|
|
@@ -1037,12 +1141,19 @@ function buildPendingRescueEnvelope({ sessionId, taskId = null, threadId = 'main
|
|
|
1037
1141
|
sessionId,
|
|
1038
1142
|
taskId,
|
|
1039
1143
|
trigger: 'pending_rescue',
|
|
1144
|
+
...(leftover?.persistedMessage ? {
|
|
1145
|
+
_persistedUserMessage: leftover.persistedMessage,
|
|
1146
|
+
_inboundAlreadyPersisted: true,
|
|
1147
|
+
_causalRootId: leftover.causalRootId || leftover.persistedMessage.id,
|
|
1148
|
+
} : {}),
|
|
1040
1149
|
msg: {
|
|
1041
|
-
|
|
1150
|
+
...leftover?.sourceMessage,
|
|
1151
|
+
id: leftover?.sourceMessage?.id || followUpId,
|
|
1042
1152
|
from: leftoverIsInternal && leftover.senderVpId ? leftover.senderVpId : 'user',
|
|
1043
1153
|
role: leftoverIsInternal ? 'assistant' : 'user',
|
|
1044
1154
|
text: replayText,
|
|
1045
1155
|
meta: {
|
|
1156
|
+
...leftover?.sourceMessage?.meta,
|
|
1046
1157
|
rescuedFrom: 'pendingQueries',
|
|
1047
1158
|
threadId,
|
|
1048
1159
|
...(leftoverInjectedBy ? { injectedBy: leftoverInjectedBy } : {}),
|
|
@@ -2067,33 +2178,39 @@ function enqueueForVp(sessionId, vpId, envelope) {
|
|
|
2067
2178
|
registerRoutePromise(envelope?.msg?.id, routePromise);
|
|
2068
2179
|
}
|
|
2069
2180
|
|
|
2070
|
-
function scheduleTaskResultRescue({ taskId, sessionId, vpId, threadId = 'main', content, taskKind, taskStatus }) {
|
|
2181
|
+
function scheduleTaskResultRescue({ taskId, sessionId, vpId, threadId = 'main', content, taskKind, taskStatus, causalRootId = null }) {
|
|
2071
2182
|
if (!sessionId || !vpId || !taskId) return false;
|
|
2072
2183
|
const text = typeof content === 'string'
|
|
2073
2184
|
? content
|
|
2074
2185
|
: (() => { try { return JSON.stringify(content); } catch { return String(content); } })();
|
|
2075
2186
|
if (!text.trim()) return false;
|
|
2076
2187
|
const msgId = `task_result_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
2188
|
+
const releaseActivity = beginSessionActivity(sessionId);
|
|
2077
2189
|
queueMicrotask(() => {
|
|
2078
|
-
|
|
2079
|
-
sessionId,
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2190
|
+
try {
|
|
2191
|
+
enqueueForVp(sessionId, vpId, {
|
|
2192
|
+
sessionId,
|
|
2193
|
+
taskId,
|
|
2194
|
+
trigger: 'task_result',
|
|
2195
|
+
_causalRootId: causalRootId,
|
|
2196
|
+
_promptSuffix: '',
|
|
2197
|
+
msg: {
|
|
2198
|
+
id: msgId,
|
|
2199
|
+
from: 'tool',
|
|
2200
|
+
role: 'assistant',
|
|
2201
|
+
text,
|
|
2202
|
+
meta: {
|
|
2203
|
+
injectedBy: 'task_result',
|
|
2204
|
+
taskId,
|
|
2205
|
+
taskKind,
|
|
2206
|
+
taskStatus,
|
|
2207
|
+
sourceThreadId: threadId,
|
|
2208
|
+
},
|
|
2094
2209
|
},
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2210
|
+
});
|
|
2211
|
+
} finally {
|
|
2212
|
+
releaseActivity();
|
|
2213
|
+
}
|
|
2097
2214
|
});
|
|
2098
2215
|
return true;
|
|
2099
2216
|
}
|
|
@@ -2130,6 +2247,7 @@ function scheduleTaskResultReentry(event) {
|
|
|
2130
2247
|
threadId,
|
|
2131
2248
|
taskKind: task.kind,
|
|
2132
2249
|
taskStatus: task.status,
|
|
2250
|
+
causalRootId: task.source?.causalRootId || null,
|
|
2133
2251
|
});
|
|
2134
2252
|
if (accepted) return;
|
|
2135
2253
|
} catch {
|
|
@@ -2146,10 +2264,20 @@ function scheduleTaskResultReentry(event) {
|
|
|
2146
2264
|
content: formatted,
|
|
2147
2265
|
taskKind: task.kind,
|
|
2148
2266
|
taskStatus: task.status,
|
|
2267
|
+
causalRootId: task.source?.causalRootId || null,
|
|
2149
2268
|
});
|
|
2150
2269
|
}
|
|
2151
2270
|
|
|
2152
2271
|
async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
2272
|
+
const releaseActivity = beginSessionActivity(sessionId);
|
|
2273
|
+
try {
|
|
2274
|
+
return await routeActiveEnvelopeToVpThread(sessionId, vpId, envelope);
|
|
2275
|
+
} finally {
|
|
2276
|
+
releaseActivity();
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
async function routeActiveEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
2153
2281
|
const routeStart = perfNowMs();
|
|
2154
2282
|
const { text, prompt, promptParts } = buildVpPromptPayload(vpId, envelope);
|
|
2155
2283
|
const runningThreads = getRunningThreads(sessionId, vpId);
|
|
@@ -2225,7 +2353,7 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
|
2225
2353
|
const content = promptParts || prompt;
|
|
2226
2354
|
const injectedBy = envelope?.msg?.meta?.injectedBy;
|
|
2227
2355
|
const isInternalAppend = injectedBy === 'route_forward' || injectedBy === 'task_result';
|
|
2228
|
-
|
|
2356
|
+
const pending = {
|
|
2229
2357
|
content,
|
|
2230
2358
|
preview: prompt,
|
|
2231
2359
|
originalText: text,
|
|
@@ -2234,7 +2362,9 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
|
2234
2362
|
injectedBy: isInternalAppend ? injectedBy : null,
|
|
2235
2363
|
senderVpId: isInternalAppend ? (envelope?.msg?.meta?.senderVpId || envelope?.msg?.from || null) : null,
|
|
2236
2364
|
sourceThreadId: isInternalAppend ? visibleInboundThreadId(envelope, thread.threadId) : null,
|
|
2237
|
-
|
|
2365
|
+
sourceMessage: envelope?.msg,
|
|
2366
|
+
};
|
|
2367
|
+
thread.pendingQueries.push(pending);
|
|
2238
2368
|
// A thread parked on a background task has no adapter activity to poll this
|
|
2239
2369
|
// queue. Wake its Engine explicitly so the new user message is consumed
|
|
2240
2370
|
// immediately while the task remains tracked in TaskManager. If the Engine
|
|
@@ -2260,6 +2390,9 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
|
2260
2390
|
ts: envelope?.msg?.ts || null,
|
|
2261
2391
|
clientMessageId: envelope?.msg?.meta?.clientMessageId || null,
|
|
2262
2392
|
});
|
|
2393
|
+
pending.persistedMessage = envelope?._persistedUserMessage || null;
|
|
2394
|
+
pending.persisted = Boolean(pending.persistedMessage);
|
|
2395
|
+
pending.causalRootId = envelope?._causalRootId || pending.persistedMessage?.id || null;
|
|
2263
2396
|
thread.updatedAt = Date.now();
|
|
2264
2397
|
try {
|
|
2265
2398
|
sendSessionEvent({
|
|
@@ -2318,6 +2451,7 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
|
2318
2451
|
function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
|
|
2319
2452
|
const key = threadKey(sessionId, vpId, threadId);
|
|
2320
2453
|
if (vpDrivers.has(key)) return;
|
|
2454
|
+
const releaseActivity = beginSessionActivity(sessionId);
|
|
2321
2455
|
const promise = (async () => {
|
|
2322
2456
|
while (true) {
|
|
2323
2457
|
const inbox = vpInboxes.get(key);
|
|
@@ -2475,10 +2609,12 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
|
|
|
2475
2609
|
}
|
|
2476
2610
|
}
|
|
2477
2611
|
}
|
|
2612
|
+
})().finally(() => {
|
|
2478
2613
|
vpDrivers.delete(key);
|
|
2479
2614
|
const tail = vpInboxes.get(key);
|
|
2480
2615
|
if (tail && tail.length > 0) ensureDriverRunning(sessionId, vpId, threadId);
|
|
2481
|
-
|
|
2616
|
+
releaseActivity();
|
|
2617
|
+
});
|
|
2482
2618
|
vpDrivers.set(key, promise);
|
|
2483
2619
|
}
|
|
2484
2620
|
|
|
@@ -2522,6 +2658,8 @@ export async function __testResetVpState() {
|
|
|
2522
2658
|
vpEngines.clear();
|
|
2523
2659
|
vpEngineConfigKeys.clear();
|
|
2524
2660
|
asyncTaskOwners.clear();
|
|
2661
|
+
sessionCopyEpochs.clear();
|
|
2662
|
+
modelReentryActivity.clear();
|
|
2525
2663
|
vpAborts.clear();
|
|
2526
2664
|
sessionContexts.clear();
|
|
2527
2665
|
projectContextBySession.clear();
|
|
@@ -3294,11 +3432,12 @@ function decorateSessionsWithRuntimeState(sessions) {
|
|
|
3294
3432
|
return rows.map(session => {
|
|
3295
3433
|
if (!session || !session.id) return session;
|
|
3296
3434
|
const runtime = bySession.get(session.id);
|
|
3297
|
-
|
|
3435
|
+
const hasWork = sessionHasActiveWork(session.id);
|
|
3436
|
+
if (!runtime) return { ...session, running: hasWork, active: hasWork };
|
|
3298
3437
|
return {
|
|
3299
3438
|
...session,
|
|
3300
|
-
running: !!runtime.running,
|
|
3301
|
-
active: !!runtime.running,
|
|
3439
|
+
running: hasWork || !!runtime.running,
|
|
3440
|
+
active: hasWork || !!runtime.running,
|
|
3302
3441
|
runningVpCount: runtime.runningVpCount || 0,
|
|
3303
3442
|
latestActivityAt: runtime.latestActivityAt || null,
|
|
3304
3443
|
};
|
|
@@ -3677,10 +3816,15 @@ export function handleYeaftCopySession(msg) {
|
|
|
3677
3816
|
const source = decorateSessionsWithRuntimeState(snapshotSessions(yeaftDir))
|
|
3678
3817
|
.find(session => session?.id === sessionId);
|
|
3679
3818
|
if (!source) throw new SessionCrudError('not_found', sessionId);
|
|
3680
|
-
|
|
3819
|
+
const active = sessionHasActiveWork(sessionId);
|
|
3820
|
+
const epoch = sessionCopyEpochs.get(sessionId);
|
|
3821
|
+
if ((active || source.running) && !epoch?.boundary) {
|
|
3822
|
+
throw new SessionCrudError('session_running', sessionId, 'No safe copy boundary for this running Session');
|
|
3823
|
+
}
|
|
3681
3824
|
const session = copySession(yeaftDir, sessionId, {
|
|
3682
3825
|
...configuredVpPaths(),
|
|
3683
3826
|
name: msg && msg.name,
|
|
3827
|
+
...(epoch?.boundary ? { throughBoundary: JSON.parse(epoch.boundary) } : {}),
|
|
3684
3828
|
...(msg && Object.prototype.hasOwnProperty.call(msg, 'throughTurnId')
|
|
3685
3829
|
? { throughTurnId: msg.throughTurnId }
|
|
3686
3830
|
: {}),
|
|
@@ -3694,6 +3838,33 @@ export function handleYeaftCopySession(msg) {
|
|
|
3694
3838
|
}
|
|
3695
3839
|
}
|
|
3696
3840
|
|
|
3841
|
+
/** Merge creates a new Session; neither source runtime is stopped or retired. */
|
|
3842
|
+
export function handleYeaftMergeSessions(msg) {
|
|
3843
|
+
const requestId = msg?.requestId;
|
|
3844
|
+
const sessionId = msg?.sessionId;
|
|
3845
|
+
const otherSessionId = msg?.otherSessionId;
|
|
3846
|
+
try {
|
|
3847
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3848
|
+
const sources = decorateSessionsWithRuntimeState(snapshotSessions(yeaftDir));
|
|
3849
|
+
for (const id of [sessionId, otherSessionId]) {
|
|
3850
|
+
const source = sources.find(row => row?.id === id);
|
|
3851
|
+
if (!source) throw new SessionCrudError('not_found', id);
|
|
3852
|
+
if (sessionHasActiveWork(id) || source.running) {
|
|
3853
|
+
throw new SessionCrudError('session_running', id, 'Cannot merge a running Session');
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
const session = mergeSessions(yeaftDir, sessionId, otherSessionId, {
|
|
3857
|
+
...configuredVpPaths(), name: msg?.name,
|
|
3858
|
+
});
|
|
3859
|
+
recordAgentSessionCreated();
|
|
3860
|
+
session.config = loadSessionConfig(yeaftDir, session.id);
|
|
3861
|
+
sendSessionCrudResult({ op: 'merge', requestId, ok: true, sourceSessionId: sessionId, otherSessionId, session });
|
|
3862
|
+
sendSessionSnapshotBroadcast();
|
|
3863
|
+
} catch (err) {
|
|
3864
|
+
sendSessionCrudResult({ op: 'merge', requestId, ok: false, sourceSessionId: sessionId, otherSessionId, error: sessionErrorPayload(err) });
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3697
3868
|
/**
|
|
3698
3869
|
* `yeaft_scan_workdir_sessions` — compatibility endpoint for the retired
|
|
3699
3870
|
* workdir Session scan flow. Session data now lives under the user-level
|
|
@@ -4008,11 +4179,14 @@ export function installYeaftRuntimeBridge(s) {
|
|
|
4008
4179
|
|
|
4009
4180
|
if (s.taskManager && typeof s.taskManager.setEventSink === 'function') {
|
|
4010
4181
|
s.taskManager.setEventSink((event) => {
|
|
4182
|
+
const releaseActivity = trackModelReentryActivity(event);
|
|
4011
4183
|
try {
|
|
4012
4184
|
const sessionId = event?.task?.sessionId || event?.sessionId || null;
|
|
4013
4185
|
sendSessionEvent(event, { sessionId });
|
|
4014
4186
|
scheduleTaskResultReentry(event);
|
|
4015
|
-
} catch { /* never let task event delivery throw */ }
|
|
4187
|
+
} catch { /* never let task event delivery throw */ } finally {
|
|
4188
|
+
releaseActivity?.();
|
|
4189
|
+
}
|
|
4016
4190
|
});
|
|
4017
4191
|
}
|
|
4018
4192
|
|
|
@@ -4932,6 +5106,21 @@ async function runYeaftSessionSend(msg) {
|
|
|
4932
5106
|
}
|
|
4933
5107
|
traceDuration('session_send.open_session', openSessionStart, { ok: !!sessionHandle });
|
|
4934
5108
|
|
|
5109
|
+
const releaseActivity = beginSessionActivity(sessionId);
|
|
5110
|
+
try {
|
|
5111
|
+
return await dispatchAcceptedSessionSend({
|
|
5112
|
+
msg, text, mentions, quote, sessionId, yeaftDir, perfTraceId, perfStart,
|
|
5113
|
+
tracePerf, traceDuration, turnConfig, sessionHandle, sessionRoot, sessionMetaForRuntime,
|
|
5114
|
+
});
|
|
5115
|
+
} finally {
|
|
5116
|
+
releaseActivity();
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
async function dispatchAcceptedSessionSend({
|
|
5121
|
+
msg, text, mentions, quote, sessionId, yeaftDir, perfTraceId, perfStart,
|
|
5122
|
+
tracePerf, traceDuration, turnConfig, sessionHandle, sessionRoot, sessionMetaForRuntime,
|
|
5123
|
+
}) {
|
|
4935
5124
|
const ensureSessionStart = perfNowMs();
|
|
4936
5125
|
await ensureSessionLoaded({ sessionMeta: sessionMetaForRuntime, perfTraceId });
|
|
4937
5126
|
if (!sessionHandle) {
|
|
@@ -5145,7 +5334,6 @@ async function runYeaftSessionSend(msg) {
|
|
|
5145
5334
|
turnId: report?.message?.id || null,
|
|
5146
5335
|
ok: true,
|
|
5147
5336
|
});
|
|
5148
|
-
|
|
5149
5337
|
}
|
|
5150
5338
|
|
|
5151
5339
|
/**
|
|
@@ -5454,7 +5642,9 @@ async function runVpTurnWithEscalation(args) {
|
|
|
5454
5642
|
const { sessionId, vpId, turnId, threadId, thread, vpAbort } = args;
|
|
5455
5643
|
const escalationState = { escalated: false, terminalEmitted: false };
|
|
5456
5644
|
|
|
5457
|
-
|
|
5645
|
+
const releaseActivity = beginSessionActivity(sessionId);
|
|
5646
|
+
const inner = runVpTurn({ ...args, escalationState }).finally(releaseActivity);
|
|
5647
|
+
await raceWithEscalation(inner, {
|
|
5458
5648
|
signal: vpAbort?.signal,
|
|
5459
5649
|
graceMs: ESCALATE_AFTER_ABORT_MS,
|
|
5460
5650
|
onEscalate: () => {
|
|
@@ -5802,6 +5992,9 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5802
5992
|
// would render the user's prompt N times.
|
|
5803
5993
|
userAlreadyPersisted: true,
|
|
5804
5994
|
currentUserMessage: inboundEnvelope?._persistedUserMessage || null,
|
|
5995
|
+
// Persist exact human ownership, not the most recently written prompt:
|
|
5996
|
+
// other VP threads can accept a new request before this one responds.
|
|
5997
|
+
causalRootId: inboundEnvelope?._causalRootId || inboundEnvelope?._persistedUserMessage?.id || null,
|
|
5805
5998
|
askUser: ({ question, options }, toolCall = null) => new Promise((resolve, reject) => {
|
|
5806
5999
|
const requestId = `ask_${randomUUID()}`;
|
|
5807
6000
|
const signal = vpAbort.signal;
|
|
@@ -6204,6 +6397,9 @@ function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistan
|
|
|
6204
6397
|
*/
|
|
6205
6398
|
function persistInboundMessageOnceByMsgId({ envelope = null, msgId, text, sessionId, threadId = 'main', role, speakerVpId, attachments, quote, internal = false, ts = null, clientMessageId = null }) {
|
|
6206
6399
|
if (!session?.conversationStore) return false;
|
|
6400
|
+
// A related input is already durable on its original thread. Rescuing it
|
|
6401
|
+
// must retain that exact row even if routing chooses another thread.
|
|
6402
|
+
if (envelope?._inboundAlreadyPersisted && envelope._persistedUserMessage?.sessionId === sessionId) return false;
|
|
6207
6403
|
// No msgId means no dedup key — caller is responsible for guarding.
|
|
6208
6404
|
// Both call sites already do (`if (envMsgId && text)` and
|
|
6209
6405
|
// `if (persistedMsgId)`); refusing here keeps the helper's contract
|
|
@@ -6263,7 +6459,10 @@ function persistInboundMessageOnceByMsgId({ envelope = null, msgId, text, sessio
|
|
|
6263
6459
|
if (persistRole === 'assistant' && speakerVpId && typeof speakerVpId === 'string') {
|
|
6264
6460
|
record.speakerVpId = speakerVpId;
|
|
6265
6461
|
}
|
|
6266
|
-
if (internal)
|
|
6462
|
+
if (internal) {
|
|
6463
|
+
record.internal = true;
|
|
6464
|
+
if (envelope?._causalRootId) record.causalRootId = envelope._causalRootId;
|
|
6465
|
+
}
|
|
6267
6466
|
if (persistRole === 'user' && Array.isArray(attachments) && attachments.length > 0) {
|
|
6268
6467
|
record.attachments = attachments;
|
|
6269
6468
|
}
|
|
@@ -8054,6 +8253,8 @@ export function handleYeaftMcpReload(msg = {}) {
|
|
|
8054
8253
|
}
|
|
8055
8254
|
|
|
8056
8255
|
export const __testHooks = {
|
|
8256
|
+
sessionHasActiveWork,
|
|
8257
|
+
scheduleTaskResultRescue,
|
|
8057
8258
|
loadProjects,
|
|
8058
8259
|
sharedProjectContext,
|
|
8059
8260
|
normalizeProjectContext,
|