@yeaft/webchat-agent 0.1.612 → 0.1.613

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/unify/engine.js +40 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.612",
3
+ "version": "0.1.613",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/unify/engine.js CHANGED
@@ -1284,13 +1284,23 @@ export class Engine {
1284
1284
  // Detach: never await. The promise outlives this query() and
1285
1285
  // the next call will pick it up (or use the fallback stub if
1286
1286
  // it hasn't resolved by then).
1287
- promise.catch(() => { /* swallow until carry-forward */ });
1288
- this.#pendingT2.set(queryNumber, {
1287
+ // PR-L follow-up: latch a synchronously-readable ready flag
1288
+ // and result on the info record so `#applyPendingT2Reflections`
1289
+ // can decide ready-vs-pending without racing microtasks.
1290
+ const info = {
1289
1291
  promise,
1290
1292
  loopRange: [arcStart, arcEnd],
1291
1293
  count: pairs.length,
1292
1294
  originalUserMsg: prompt,
1293
- });
1295
+ ready: false,
1296
+ result: null,
1297
+ error: null,
1298
+ };
1299
+ promise.then(
1300
+ (v) => { info.ready = true; info.result = v; },
1301
+ (err) => { info.ready = true; info.error = err; },
1302
+ );
1303
+ this.#pendingT2.set(queryNumber, info);
1294
1304
  }
1295
1305
  }
1296
1306
 
@@ -1328,10 +1338,16 @@ export class Engine {
1328
1338
  // by the Anthropic / OpenAI Responses APIs stays intact. We
1329
1339
  // don't block the call — the LLM still decides.
1330
1340
  const dupHash = argsHashOf(tc.input);
1341
+ // PR-L follow-up: lookback is by user-conversation turn
1342
+ // (`queryNumber`), NOT by inner adapter loop iteration. Each call
1343
+ // to query() bumps queryNumber once, so "last 2 turns" means the
1344
+ // current user turn + the previous two user turns — the natural
1345
+ // semantic for "the model is stuck in a loop across the
1346
+ // conversation."
1331
1347
  const dupInfo = this.#execLog.dupInfo({
1332
1348
  toolName: tc.name,
1333
1349
  argsHash: dupHash,
1334
- currentTurn: turnNumber,
1350
+ currentTurn: queryNumber,
1335
1351
  lookbackTurns: 2,
1336
1352
  });
1337
1353
  if (dupInfo.count + 1 >= DUP_TOOL_THRESHOLD) {
@@ -1393,7 +1409,11 @@ export class Engine {
1393
1409
  // PR-L: persist this execution to the exec-log for fallback-stub
1394
1410
  // and duplicate-call detection. Best-effort — disk failures are
1395
1411
  // swallowed inside ExecLog.append.
1396
- this.#execLog.append(turnNumber, buildExecLogEntry({
1412
+ // PR-L follow-up: persist under `queryNumber` (one entry-key per
1413
+ // user-conversation turn), not the inner loop's turnNumber.
1414
+ // Aligns exec-log layout with dup detection lookback and the
1415
+ // T2 fallback-stub readTurn() call below.
1416
+ this.#execLog.append(queryNumber, buildExecLogEntry({
1397
1417
  loopIdx: queryToolCount,
1398
1418
  toolName: tc.name,
1399
1419
  args: tc.input,
@@ -1567,33 +1587,29 @@ export class Engine {
1567
1587
  continue;
1568
1588
  }
1569
1589
 
1570
- // Non-blocking readiness check: race the promise against an already-
1571
- // settled Promise.resolve(SENTINEL). If the reflection promise wins,
1572
- // it has resolved synchronously (microtask order). Otherwise it's
1573
- // still pending and we use the fallback stub.
1574
- const PENDING_SENTINEL = {};
1575
- const settled = await Promise.race([
1576
- info.promise.then((v) => ({ ok: true, content: v.content, durationMs: v.durationMs }))
1577
- .catch((err) => ({ ok: false, err })),
1578
- Promise.resolve(PENDING_SENTINEL),
1579
- ]);
1580
-
1590
+ // PR-L follow-up: deterministic readiness check. The info record
1591
+ // carries `ready / result / error` flags that are flipped from the
1592
+ // promise's then/catch handler; reading them here is purely
1593
+ // synchronous bookkeeping no microtask race.
1581
1594
  let content;
1582
1595
  let trigger;
1583
1596
  let durationMs = 0;
1584
- if (settled === PENDING_SENTINEL) {
1585
- // Stub fallback. Detach the unresolved promise so we don't keep
1586
- // holding it (caller may still observe it via .catch elsewhere).
1587
- info.promise.catch(() => { /* swallow late rejection */ });
1597
+ if (!info.ready) {
1598
+ // Still in flight fall back to the exec-log stub. Detach the
1599
+ // unresolved promise so we don't leak it (handlers above already
1600
+ // swallow rejection by routing into info.error).
1588
1601
  const entries = this.#execLog ? this.#execLog.readTurn(turnNumber) : [];
1589
1602
  content = buildFallbackStub({ execLogEntries: entries, originalUserMsg: info.originalUserMsg || originalUserMsg });
1590
1603
  trigger = 't2-fallback';
1591
- } else if (settled.ok) {
1592
- content = settled.content;
1604
+ } else if (info.error) {
1605
+ // Promise rejected — leave history unchanged, no event.
1606
+ continue;
1607
+ } else if (info.result && typeof info.result.content === 'string' && info.result.content) {
1608
+ content = info.result.content;
1593
1609
  trigger = 't2';
1594
- durationMs = settled.durationMs || 0;
1610
+ durationMs = info.result.durationMs || 0;
1595
1611
  } else {
1596
- // Promise rejectedleave history unchanged.
1612
+ // Resolved but with no usable content defensively skip.
1597
1613
  continue;
1598
1614
  }
1599
1615