@yeaft/webchat-agent 0.1.771 → 0.1.772
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/package.json +1 -1
- package/unify/web-bridge.js +88 -2
package/package.json
CHANGED
package/unify/web-bridge.js
CHANGED
|
@@ -55,6 +55,23 @@ import { createVpStatusBroker } from './vp-status-broker.js';
|
|
|
55
55
|
/** @type {import('./session.js').Session | null} */
|
|
56
56
|
let session = null;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Tracks scoped-dream triggers that are currently inflight, keyed by
|
|
60
|
+
* groupId. Used by `handleUnifyDreamTrigger` to reject any overlapping
|
|
61
|
+
* scoped trigger rather than racing the sink-wrapping logic against
|
|
62
|
+
* itself.
|
|
63
|
+
*
|
|
64
|
+
* Cross-group overlap is rejected (not just same-group): under the
|
|
65
|
+
* existing dream scheduler a second concurrent trigger silently shares
|
|
66
|
+
* the first's inflight promise and dropped its own scope filter. So
|
|
67
|
+
* "B during A's run" doesn't actually produce a separate scoped pass
|
|
68
|
+
* for B — letting B install a second sink wrapper would only mis-stamp
|
|
69
|
+
* A's events with B's groupId. Rejecting B with an explicit error is
|
|
70
|
+
* the honest answer; the user can re-click after A settles.
|
|
71
|
+
* @type {Set<string>}
|
|
72
|
+
*/
|
|
73
|
+
const inflightScopedDreamGroups = new Set();
|
|
74
|
+
|
|
58
75
|
/**
|
|
59
76
|
* Single in-flight AbortController. A new user message cancels the prior
|
|
60
77
|
* round (if any). H2.f.2: replaces the per-thread Map.
|
|
@@ -1185,9 +1202,23 @@ export function installUnifyRuntimeBridge(s) {
|
|
|
1185
1202
|
if (!s) return;
|
|
1186
1203
|
|
|
1187
1204
|
// Forward dream pipeline progress events to the web debug panel.
|
|
1205
|
+
//
|
|
1206
|
+
// Group-id stamping is NO LONGER done here. It used to be: this sink
|
|
1207
|
+
// read a module-level `activeScopedDreamGroupId` that
|
|
1208
|
+
// `handleUnifyDreamTrigger({groupId})` parked before awaiting the
|
|
1209
|
+
// scope-filtered pass. That created a race when two scoped triggers
|
|
1210
|
+
// overlapped (auto-tick during a manual click; or two manual clicks
|
|
1211
|
+
// for different groups): the second handler's `finally` could clear
|
|
1212
|
+
// the module slot while the first run was still emitting events,
|
|
1213
|
+
// dropping the stamp from the tail of the first pass. The new design:
|
|
1214
|
+
// `handleUnifyDreamTrigger` wraps THIS sink for the lifetime of the
|
|
1215
|
+
// trigger to inject `groupId` per-call (see that function below). The
|
|
1216
|
+
// base sink is intentionally a pure passthrough.
|
|
1188
1217
|
s._dreamProgressSink = (evt) => {
|
|
1189
1218
|
try {
|
|
1190
|
-
|
|
1219
|
+
const out = { type: 'dream_progress', ...evt };
|
|
1220
|
+
const tag = evt && evt.groupId ? { groupId: evt.groupId } : {};
|
|
1221
|
+
sendUnifyEvent(out, tag);
|
|
1191
1222
|
} catch { /* never let event delivery throw */ }
|
|
1192
1223
|
};
|
|
1193
1224
|
|
|
@@ -2660,6 +2691,46 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2660
2691
|
return;
|
|
2661
2692
|
}
|
|
2662
2693
|
|
|
2694
|
+
// Concurrent-trigger guard for scoped runs. Two scoped clicks (same
|
|
2695
|
+
// group or different) overlapping the same inflight pass used to set
|
|
2696
|
+
// the module-level groupId slot, race the sink wrapping, and let the
|
|
2697
|
+
// second `finally` restore the original sink while the first run was
|
|
2698
|
+
// still emitting events. We now refuse any second scoped trigger
|
|
2699
|
+
// while ANY scoped pass is inflight — the scheduler already
|
|
2700
|
+
// short-circuits the underlying run for same-group, and a different
|
|
2701
|
+
// group's filter would have been silently dropped anyway (see
|
|
2702
|
+
// dream-v2/schedule.js inflight reuse), so the user-facing semantics
|
|
2703
|
+
// are unchanged ("you already asked").
|
|
2704
|
+
if (groupId && inflightScopedDreamGroups.size > 0) {
|
|
2705
|
+
sendToServer({
|
|
2706
|
+
type: 'unify_dream_result',
|
|
2707
|
+
...tag,
|
|
2708
|
+
success: false,
|
|
2709
|
+
error: 'A dream pass is already running.',
|
|
2710
|
+
});
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
// Per-call sink wrapper. For scoped runs we install a closure that
|
|
2715
|
+
// injects this trigger's groupId onto top-level events the runner
|
|
2716
|
+
// emits without one (start/merge/done), then delegates to the
|
|
2717
|
+
// original passthrough sink. The wrapper lives only for the lifetime
|
|
2718
|
+
// of this trigger and is restored in `finally`; concurrent calls for
|
|
2719
|
+
// OTHER groupIds chain (last-installed wins) but each restoration
|
|
2720
|
+
// unwinds back to its predecessor.
|
|
2721
|
+
const originalSink = session?._dreamProgressSink;
|
|
2722
|
+
if (groupId && typeof originalSink === 'function') {
|
|
2723
|
+
inflightScopedDreamGroups.add(groupId);
|
|
2724
|
+
session._dreamProgressSink = (evt) => {
|
|
2725
|
+
try {
|
|
2726
|
+
const stamped = evt && evt.groupId
|
|
2727
|
+
? evt
|
|
2728
|
+
: { ...evt, groupId };
|
|
2729
|
+
originalSink(stamped);
|
|
2730
|
+
} catch { /* never let event delivery throw */ }
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2663
2734
|
try {
|
|
2664
2735
|
sendToServer({
|
|
2665
2736
|
type: 'unify_dream_status',
|
|
@@ -2678,6 +2749,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2678
2749
|
const targets = Array.isArray(result?.targets) ? result.targets : [];
|
|
2679
2750
|
const entriesCreated = targets.filter(t => t && t.status === 'done').length;
|
|
2680
2751
|
const lastDreamAt = result?.startedAt || new Date().toISOString();
|
|
2752
|
+
const success = !result.error && !result.skipped;
|
|
2681
2753
|
|
|
2682
2754
|
// Spread `result` FIRST so derived fields (success, entriesCreated,
|
|
2683
2755
|
// lastDreamAt) authoritatively shadow anything the runner might grow
|
|
@@ -2685,11 +2757,19 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2685
2757
|
// { groups, targets, startedAt, error?, skipped? }) but the failure
|
|
2686
2758
|
// mode of the alternative ordering is silent — review feedback from
|
|
2687
2759
|
// PR #743.
|
|
2760
|
+
//
|
|
2761
|
+
// This `unify_dream_result` envelope is the SOLE terminal signal for
|
|
2762
|
+
// a dream pass. The chat-store projects it into BOTH `unifyDreamLatest`
|
|
2763
|
+
// (final tally row) AND `unifyDreamEvents` (ring-buffer terminal
|
|
2764
|
+
// marker), so we no longer mirror a synthetic `phase:'result'`
|
|
2765
|
+
// dream_progress event — that mirror used to race the
|
|
2766
|
+
// `unifyDreamLatest` writer and flip the success row back to
|
|
2767
|
+
// 'running' (Critical reviewer finding pre-merge).
|
|
2688
2768
|
sendToServer({
|
|
2689
2769
|
type: 'unify_dream_result',
|
|
2690
2770
|
...tag,
|
|
2691
2771
|
...result,
|
|
2692
|
-
success
|
|
2772
|
+
success,
|
|
2693
2773
|
entriesCreated,
|
|
2694
2774
|
lastDreamAt,
|
|
2695
2775
|
});
|
|
@@ -2700,6 +2780,12 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2700
2780
|
success: false,
|
|
2701
2781
|
error: err?.message || String(err),
|
|
2702
2782
|
});
|
|
2783
|
+
} finally {
|
|
2784
|
+
// Restore the original sink and release the per-group inflight lock.
|
|
2785
|
+
if (groupId && typeof originalSink === 'function') {
|
|
2786
|
+
session._dreamProgressSink = originalSink;
|
|
2787
|
+
inflightScopedDreamGroups.delete(groupId);
|
|
2788
|
+
}
|
|
2703
2789
|
}
|
|
2704
2790
|
}
|
|
2705
2791
|
|