@yeaft/webchat-agent 1.0.495 → 1.0.497

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.
@@ -76,7 +76,12 @@ import { hydrateYeaftStatusFromSession } from './status-cache.js';
76
76
  import { handleVpSubscribe } from './vp/vp-bridge.js';
77
77
  import { createVp, updateVp, deleteVp, readVp, VpCrudError } from './vp/vp-crud.js';
78
78
  import { scanVpLibrary } from './vp/vp-store.js';
79
- import { createRouter } from './routing/router.js';
79
+ import {
80
+ bindRepoApprovalCapability,
81
+ createRouter,
82
+ isRepoApprovalCapability,
83
+ revokeRepoApprovalCapability,
84
+ } from './routing/router.js';
80
85
  import {
81
86
  SessionCrudError,
82
87
  createSessionFromSpec,
@@ -462,6 +467,21 @@ const turnAbortCtrls = new Map();
462
467
  * @type {Map<string, { sessionId: string, vpId: string, threadId: string, key: string }>}
463
468
  */
464
469
  const turnAbortMeta = new Map();
470
+ /** @type {Map<string, { capability: object, sessionId: string, vpId: string }>} Exact-turn repo approvals, never persisted or projected. */
471
+ const turnRepoApprovals = new Map();
472
+
473
+ function revokeTurnRepoApproval(turnId) {
474
+ const grant = turnRepoApprovals.get(turnId);
475
+ turnRepoApprovals.delete(turnId);
476
+ return grant ? revokeRepoApprovalCapability(grant.capability) : false;
477
+ }
478
+
479
+ function revokeRepoApprovalsForRecipient(sessionId, vpId) {
480
+ for (const [turnId, grant] of turnRepoApprovals.entries()) {
481
+ if (grant.sessionId !== sessionId || grant.vpId !== vpId) continue;
482
+ revokeTurnRepoApproval(turnId);
483
+ }
484
+ }
465
485
 
466
486
  /**
467
487
  * Per-VP status broker — the agent-side authority for VP timeline
@@ -1168,6 +1188,7 @@ function invalidateGroupContext(sessionId, { abortRuntime = false } = {}) {
1168
1188
  }
1169
1189
  for (const [turnId, meta] of Array.from(turnAbortMeta.entries())) {
1170
1190
  if (meta?.sessionId !== sessionId) continue;
1191
+ revokeTurnRepoApproval(turnId);
1171
1192
  turnAbortCtrls.delete(turnId);
1172
1193
  turnAbortMeta.delete(turnId);
1173
1194
  }
@@ -2001,7 +2022,9 @@ function getOrCreateSessionContext(sessionId, sessionHandle) {
2001
2022
  const coord = createCoordinator(sessionHandle, {
2002
2023
  deliver: (vpId, envelope) => enqueueForVp(sessionId, vpId, envelope),
2003
2024
  });
2004
- const router = createRouter({ coordinator: coord });
2025
+ const router = createRouter({
2026
+ coordinator: coord,
2027
+ });
2005
2028
  if (!entry) {
2006
2029
  entry = makeGroupContextStub();
2007
2030
  sessionContexts.set(sessionId, entry);
@@ -2138,12 +2161,21 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
2138
2161
  let related = false;
2139
2162
 
2140
2163
  const meta = envelope?.msg?.meta || {};
2164
+ const repoApproval = envelope?._repoApproval;
2165
+ const hasRepoApproval = isRepoApprovalCapability(repoApproval);
2166
+ if (envelope?.msg?.role === 'user' || envelope?.msg?.from === 'user') {
2167
+ revokeRepoApprovalsForRecipient(sessionId, vpId);
2168
+ }
2141
2169
  const isTaskResult = meta.injectedBy === 'task_result';
2142
2170
  const sourceThreadId = typeof meta.sourceThreadId === 'string' && meta.sourceThreadId.trim()
2143
2171
  ? meta.sourceThreadId.trim()
2144
2172
  : null;
2145
2173
 
2146
- if (isTaskResult && sourceThreadId) {
2174
+ if (hasRepoApproval) {
2175
+ // Approval is an execution capability, not text. It must never collapse
2176
+ // into a related thread's pendingQueries/rescue path.
2177
+ thread = getOrCreateVpThread({ sessionId, vpId, title: fallbackTitle(text) });
2178
+ } else if (isTaskResult && sourceThreadId) {
2147
2179
  thread = getOrCreateVpThread({ sessionId, vpId, threadId: sourceThreadId, title: fallbackTitle(text) });
2148
2180
  } else if (runningThreads.length === 0) {
2149
2181
  thread = getOrCreateVpThread({ sessionId, vpId, title: fallbackTitle(text) });
@@ -2186,6 +2218,12 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
2186
2218
  if (!thread) return null;
2187
2219
  rememberThreadMessage(thread, envelope?.msg);
2188
2220
  const turnId = `${randomUUID().slice(0, 8)}:${vpId}`;
2221
+ if (hasRepoApproval) {
2222
+ if (!bindRepoApprovalCapability(repoApproval, { sessionId, recipientVpId: vpId, turnId })) {
2223
+ return null;
2224
+ }
2225
+ turnRepoApprovals.set(turnId, { capability: repoApproval, sessionId, vpId });
2226
+ }
2189
2227
  const perfTraceId = envelope?._perfTraceId || envelope?.perfTraceId || null;
2190
2228
  if (perfTraceId) {
2191
2229
  recordAgentPerfTrace(ctx.CONFIG, {
@@ -2357,6 +2395,7 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
2357
2395
  } catch (err) {
2358
2396
  console.warn('[Yeaft] driveVp: runVpTurn failed', vpId, err?.message || err);
2359
2397
  } finally {
2398
+ revokeTurnRepoApproval(turnId);
2360
2399
  turnAbortCtrls.delete(turnId);
2361
2400
  turnAbortMeta.delete(turnId);
2362
2401
  if (vpAborts.get(key) === vpAbort) vpAborts.delete(key);
@@ -2491,6 +2530,7 @@ export async function __testResetVpState() {
2491
2530
  if (Array.isArray(inbox)) inbox.length = 0;
2492
2531
  }
2493
2532
  vpThreads.clear();
2533
+ for (const turnId of turnRepoApprovals.keys()) revokeTurnRepoApproval(turnId);
2494
2534
  turnAbortCtrls.clear();
2495
2535
  turnAbortMeta.clear();
2496
2536
  await __testDrainVpDrivers();
@@ -3921,7 +3961,6 @@ function buildVpPersona(vpId) {
3921
3961
  }
3922
3962
  }
3923
3963
 
3924
-
3925
3964
  /**
3926
3965
  * Install the dream pipeline progress sink and runtime settings bridge.
3927
3966
  * Thread scheduling is owned by the group VP runtime below, not by mutable
@@ -5046,7 +5085,7 @@ function resolveCollabToolPolicy(sessionMeta) {
5046
5085
  return vpCount > 1 ? COLLAB_TOOL_POLICY.MULTI_VP : COLLAB_TOOL_POLICY.SINGLE_VP;
5047
5086
  }
5048
5087
 
5049
- export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope, threadId = 'main' }) {
5088
+ export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope, threadId = 'main', turnId = null }) {
5050
5089
  // Read the session meta once and reuse for defaultVpId fallback,
5051
5090
  // announcement injection, and roster prompt context. Calling getMeta()
5052
5091
  // twice per turn is wasteful — and (more importantly) opens a window
@@ -5083,6 +5122,7 @@ export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope
5083
5122
  if (!resolvedVpId) return undefined;
5084
5123
 
5085
5124
  const out = { senderVpId: resolvedVpId, threadId: threadId || 'main' };
5125
+ if (typeof turnId === 'string' && turnId.trim()) out.vpTurnId = turnId.trim();
5086
5126
  if (typeof sessionId === 'string' && sessionId.trim()) {
5087
5127
  out.sessionId = sessionId.trim();
5088
5128
  }
@@ -5110,7 +5150,9 @@ export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope
5110
5150
  if (persona) out.vpPersona = persona;
5111
5151
  if (sessionCoordinator && typeof sessionCoordinator.ingest === 'function') {
5112
5152
  try {
5113
- out.router = createRouter({ coordinator: sessionCoordinator });
5153
+ out.router = createRouter({
5154
+ coordinator: sessionCoordinator,
5155
+ });
5114
5156
  } catch {
5115
5157
  // Router build failure is non-fatal.
5116
5158
  }
@@ -5304,6 +5346,7 @@ async function runVpTurnWithEscalation(args) {
5304
5346
  graceMs: ESCALATE_AFTER_ABORT_MS,
5305
5347
  onEscalate: () => {
5306
5348
  if (escalationState.escalated) return;
5349
+ revokeTurnRepoApproval(turnId);
5307
5350
  escalationState.escalated = true;
5308
5351
  escalationState.terminalEmitted = true;
5309
5352
  console.error(
@@ -5522,6 +5565,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5522
5565
  queryTimer = setTimeout(() => {
5523
5566
  if (!vpAbort.signal.aborted) {
5524
5567
  console.error(`[Yeaft] query timeout after ${queryTimeoutMs / 1000}s of silence — aborting VP ${vpId}`);
5568
+ revokeTurnRepoApproval(turnId);
5525
5569
  try { vpAbort.abort(); } catch { /* best-effort */ }
5526
5570
  }
5527
5571
  }, queryTimeoutMs);
@@ -5558,6 +5602,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5558
5602
  sessionId,
5559
5603
  envelope: inboundEnvelope,
5560
5604
  threadId,
5605
+ turnId,
5561
5606
  });
5562
5607
  if (queryOpts) {
5563
5608
  const envelopeProjectContext = normalizeProjectContext(inboundEnvelope?._projectContext, sessionId);
@@ -6219,6 +6264,10 @@ function emitQueuedTurnAbort(meta, turnId) {
6219
6264
  }
6220
6265
 
6221
6266
  function abortAllVpRuntime(aborted, sessionId = null) {
6267
+ for (const [turnId, meta] of Array.from(turnAbortMeta.entries())) {
6268
+ if (sessionId && meta?.sessionId !== sessionId) continue;
6269
+ revokeTurnRepoApproval(turnId);
6270
+ }
6222
6271
  for (const [key, ctrl] of vpAborts) {
6223
6272
  if (sessionId && !key.startsWith(`${sessionId}::`)) continue;
6224
6273
  try {
@@ -6261,6 +6310,7 @@ export function handleYeaftAbortThread(_msg = {}) {
6261
6310
  const meta = turnAbortMeta.get(turnId);
6262
6311
  if ((meta?.threadId || 'main') !== targetThreadId) continue;
6263
6312
  try { if (!ctrl.signal.aborted) { ctrl.abort(); aborted.push(turnId); } } catch { /* best-effort */ }
6313
+ revokeTurnRepoApproval(turnId);
6264
6314
  turnAbortCtrls.delete(turnId);
6265
6315
  turnAbortMeta.delete(turnId);
6266
6316
  }
@@ -6279,6 +6329,7 @@ export function handleYeaftAbortThread(_msg = {}) {
6279
6329
  for (const [turnId, ctrl] of turnAbortCtrls) {
6280
6330
  try { if (!ctrl.signal.aborted) { ctrl.abort(); aborted.push(turnId); } } catch { /* best-effort */ }
6281
6331
  }
6332
+ for (const turnId of turnAbortMeta.keys()) revokeTurnRepoApproval(turnId);
6282
6333
  turnAbortCtrls.clear();
6283
6334
  turnAbortMeta.clear();
6284
6335
  abortAllVpRuntime(aborted);
@@ -6305,10 +6356,12 @@ export function handleYeaftAbortAll(msg = {}) {
6305
6356
  const meta = turnAbortMeta.get(turnId);
6306
6357
  if (sessionId && meta?.sessionId !== sessionId) continue;
6307
6358
  try { if (!ctrl.signal.aborted) { ctrl.abort(); aborted.push(turnId); } } catch { /* best-effort */ }
6359
+ revokeTurnRepoApproval(turnId);
6308
6360
  turnAbortCtrls.delete(turnId);
6309
6361
  turnAbortMeta.delete(turnId);
6310
6362
  }
6311
6363
  if (!sessionId) {
6364
+ for (const turnId of turnAbortMeta.keys()) revokeTurnRepoApproval(turnId);
6312
6365
  turnAbortCtrls.clear();
6313
6366
  turnAbortMeta.clear();
6314
6367
  }
@@ -6355,6 +6408,7 @@ export function handleYeaftAbortTurn(msg = {}) {
6355
6408
  success = true;
6356
6409
  abortedTurnIds.push(turnId);
6357
6410
  }
6411
+ revokeTurnRepoApproval(turnId);
6358
6412
  turnAbortCtrls.delete(turnId);
6359
6413
  turnAbortMeta.delete(turnId);
6360
6414
  }
@@ -7665,6 +7719,7 @@ export async function resetYeaftSession() {
7665
7719
  try { if (!ctrl.signal.aborted) ctrl.abort(); } catch { /* best-effort */ }
7666
7720
  }
7667
7721
  vpAborts.clear();
7722
+ for (const turnId of turnAbortMeta.keys()) revokeTurnRepoApproval(turnId);
7668
7723
  turnAbortCtrls.clear();
7669
7724
  turnAbortMeta.clear();
7670
7725
  vpInboxes.clear();
@@ -8143,6 +8198,7 @@ export const __testHooks = {
8143
8198
  return loadAndBroadcastYeaftSkillSlashCommands();
8144
8199
  },
8145
8200
  resetAbortState() {
8201
+ for (const turnId of turnRepoApprovals.keys()) revokeTurnRepoApproval(turnId);
8146
8202
  turnAbortCtrls.clear();
8147
8203
  turnAbortMeta.clear();
8148
8204
  vpAborts.clear();
@@ -8208,7 +8264,9 @@ export const __testHooks = {
8208
8264
  });
8209
8265
  const entry = makeGroupContextStub();
8210
8266
  entry.coord = coord;
8211
- entry.router = createRouter({ coordinator: coord });
8267
+ entry.router = createRouter({
8268
+ coordinator: coord,
8269
+ });
8212
8270
  entry.sessionHandle = group;
8213
8271
  entry.historyHydrated = true;
8214
8272
  sessionContexts.set(sessionId, entry);