instar 1.3.72 → 1.3.74
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +24 -4
- package/dist/commands/server.js.map +1 -1
- package/dist/core/SleepWakeDetector.d.ts +83 -5
- package/dist/core/SleepWakeDetector.d.ts.map +1 -1
- package/dist/core/SleepWakeDetector.js +117 -11
- package/dist/core/SleepWakeDetector.js.map +1 -1
- package/dist/core/UsherActedCorrelator.d.ts +96 -0
- package/dist/core/UsherActedCorrelator.d.ts.map +1 -0
- package/dist/core/UsherActedCorrelator.js +175 -0
- package/dist/core/UsherActedCorrelator.js.map +1 -0
- package/dist/core/UsherSignalStore.d.ts +26 -2
- package/dist/core/UsherSignalStore.d.ts.map +1 -1
- package/dist/core/UsherSignalStore.js +17 -2
- package/dist/core/UsherSignalStore.js.map +1 -1
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +6 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +2 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +8 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +26 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/server/usherRoutes.d.ts.map +1 -1
- package/dist/server/usherRoutes.js +7 -1
- package/dist/server/usherRoutes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/src/scaffold/templates.ts +6 -0
- package/upgrades/1.3.73.md +73 -0
- package/upgrades/1.3.74.md +95 -0
- package/upgrades/side-effects/usher-precision-loop.md +131 -0
|
@@ -6,6 +6,32 @@
|
|
|
6
6
|
* We detect this drift and fire a callback.
|
|
7
7
|
*
|
|
8
8
|
* Ported from Dawn's infrastructure — battle-tested in production.
|
|
9
|
+
*
|
|
10
|
+
* ## CPU-starvation guard (2026-05-28)
|
|
11
|
+
*
|
|
12
|
+
* Timer drift has TWO causes, not one:
|
|
13
|
+
* 1. Real sleep — the OS suspends the process; the wall clock jumps forward.
|
|
14
|
+
* 2. CPU starvation — the machine is so oversubscribed (load >> cores) that the
|
|
15
|
+
* event loop can't service the `checkIntervalMs` timer on time. The wall
|
|
16
|
+
* clock advances normally, but the callback fires seconds late.
|
|
17
|
+
*
|
|
18
|
+
* The original detector could not tell these apart, so on a heavily-loaded box
|
|
19
|
+
* (e.g. many concurrent agent sessions) it fired hundreds of false "wake" events
|
|
20
|
+
* — each triggering expensive wake-recovery (tunnel restart, re-registration,
|
|
21
|
+
* failure-counter resets) that piled MORE load on, a self-reinforcing storm.
|
|
22
|
+
*
|
|
23
|
+
* The guard distinguishes the two:
|
|
24
|
+
* - A **long** drift (>= `longSleepFloorSeconds`) is unambiguously real sleep —
|
|
25
|
+
* a live machine never starves its own event loop for minutes (a watchdog
|
|
26
|
+
* would declare it dead first). Always emitted, regardless of load.
|
|
27
|
+
* - A **short** drift under high system load (`loadavg[0] / cpuCount >
|
|
28
|
+
* maxLoadRatio`) is treated as CPU starvation and SUPPRESSED — no `wake`.
|
|
29
|
+
* - A short drift under normal load is a brief real sleep — emitted.
|
|
30
|
+
* On top of classification, a `minWakeIntervalMs` cooldown caps the emit rate so
|
|
31
|
+
* even a misclassified burst can't trigger a recovery storm.
|
|
32
|
+
*
|
|
33
|
+
* Suppressed events are NOT added to `wakeHistory`, so `getCumulativeSleepMsBetween`
|
|
34
|
+
* (the wake-reaper's sleep-credit source) only ever counts genuine sleep.
|
|
9
35
|
*/
|
|
10
36
|
import { EventEmitter } from 'node:events';
|
|
11
37
|
export interface SleepWakeDetectorConfig {
|
|
@@ -13,20 +39,73 @@ export interface SleepWakeDetectorConfig {
|
|
|
13
39
|
checkIntervalMs?: number;
|
|
14
40
|
/** How much drift (ms) indicates a sleep event. Default: 10000 */
|
|
15
41
|
driftThresholdMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Above this `loadavg[0] / cpuCount` ratio, a SHORT drift is classified as CPU
|
|
44
|
+
* starvation and suppressed rather than emitted as a wake. Default: 1.5.
|
|
45
|
+
* Set to `Infinity` to disable the load guard (always trust the drift).
|
|
46
|
+
*/
|
|
47
|
+
maxLoadRatio?: number;
|
|
48
|
+
/**
|
|
49
|
+
* A drift at least this long (seconds) is always treated as real sleep,
|
|
50
|
+
* regardless of load — a live event loop never starves for this long.
|
|
51
|
+
* Default: 300 (5 minutes).
|
|
52
|
+
*/
|
|
53
|
+
longSleepFloorSeconds?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Minimum gap (ms) between EMITTED wake events. A short drift that would emit
|
|
56
|
+
* within this cooldown of the previous emitted wake is suppressed, bounding
|
|
57
|
+
* recovery storms. Long sleeps bypass the cooldown. Default: 60000 (1 min).
|
|
58
|
+
*/
|
|
59
|
+
minWakeIntervalMs?: number;
|
|
60
|
+
/** Injectable system-load source (testing). Default: os.loadavg. */
|
|
61
|
+
loadAvgProvider?: () => number[];
|
|
62
|
+
/** Injectable CPU-count source (testing). Default: os.cpus().length. */
|
|
63
|
+
cpuCountProvider?: () => number;
|
|
64
|
+
/** Injectable wall-clock source (testing). Default: Date.now. */
|
|
65
|
+
nowProvider?: () => number;
|
|
16
66
|
}
|
|
17
67
|
export interface WakeEvent {
|
|
18
68
|
sleepDurationSeconds: number;
|
|
19
69
|
timestamp: string;
|
|
20
70
|
}
|
|
71
|
+
export type WakeSuppressionReason = 'cpu-starvation' | 'cooldown';
|
|
72
|
+
export interface SuppressedWakeEvent {
|
|
73
|
+
reason: WakeSuppressionReason;
|
|
74
|
+
driftSeconds: number;
|
|
75
|
+
loadRatio: number;
|
|
76
|
+
timestamp: string;
|
|
77
|
+
}
|
|
78
|
+
export interface SleepWakeStats {
|
|
79
|
+
wakeCount: number;
|
|
80
|
+
totalSleepSeconds: number;
|
|
81
|
+
longestSleepSeconds: number;
|
|
82
|
+
/** Drifts classified as CPU starvation / rate-limited and NOT emitted. */
|
|
83
|
+
suppressedCount: number;
|
|
84
|
+
suppressedByReason: Record<WakeSuppressionReason, number>;
|
|
85
|
+
/** ISO timestamp of the most recent suppression, or null. */
|
|
86
|
+
lastSuppressedAt: string | null;
|
|
87
|
+
}
|
|
21
88
|
export declare class SleepWakeDetector extends EventEmitter {
|
|
22
89
|
private interval;
|
|
23
90
|
private lastTick;
|
|
91
|
+
private lastEmittedWakeAtMs;
|
|
24
92
|
private checkIntervalMs;
|
|
25
93
|
private driftThresholdMs;
|
|
94
|
+
private maxLoadRatio;
|
|
95
|
+
private longSleepFloorSeconds;
|
|
96
|
+
private minWakeIntervalMs;
|
|
97
|
+
private loadAvgProvider;
|
|
98
|
+
private cpuCountProvider;
|
|
99
|
+
private now;
|
|
26
100
|
private wakeHistory;
|
|
101
|
+
private suppressionHistory;
|
|
27
102
|
constructor(config?: SleepWakeDetectorConfig);
|
|
28
103
|
start(): void;
|
|
29
104
|
stop(): void;
|
|
105
|
+
/** Current `loadavg[0] / cpuCount` ratio. Returns 0 when load is unavailable
|
|
106
|
+
* (e.g. Windows reports [0,0,0]), which disables the starvation guard there. */
|
|
107
|
+
private currentLoadRatio;
|
|
108
|
+
private recordSuppression;
|
|
30
109
|
/**
|
|
31
110
|
* Cumulative wall-time-asleep during the half-open window [startMs, endMs).
|
|
32
111
|
* Used by the wake-reaper (UNIFIED-SESSION-LIFECYCLE §P0 #9 / SE-8) to subtract
|
|
@@ -38,13 +117,12 @@ export declare class SleepWakeDetector extends EventEmitter {
|
|
|
38
117
|
* [wakeTimestamp − sleepDurationSeconds, wakeTimestamp].
|
|
39
118
|
* Returns the sum of overlap with the query window in milliseconds. Returns 0
|
|
40
119
|
* when the history is empty or no event overlaps.
|
|
120
|
+
*
|
|
121
|
+
* Only EMITTED wakes are in `wakeHistory`; suppressed CPU-starvation drifts are
|
|
122
|
+
* deliberately excluded so starvation is never credited as real sleep.
|
|
41
123
|
*/
|
|
42
124
|
getCumulativeSleepMsBetween(startMs: number, endMs: number): number;
|
|
43
125
|
/** Get wake event stats for telemetry reporting. */
|
|
44
|
-
getStats(sinceMs?: number):
|
|
45
|
-
wakeCount: number;
|
|
46
|
-
totalSleepSeconds: number;
|
|
47
|
-
longestSleepSeconds: number;
|
|
48
|
-
};
|
|
126
|
+
getStats(sinceMs?: number): SleepWakeStats;
|
|
49
127
|
}
|
|
50
128
|
//# sourceMappingURL=SleepWakeDetector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SleepWakeDetector.d.ts","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"SleepWakeDetector.d.ts","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,MAAM,WAAW,uBAAuB;IACtC,uDAAuD;IACvD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IACjC,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,MAAM,CAAC;IAChC,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG,UAAU,CAAC;AAElE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;IAC1D,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,qBAAa,iBAAkB,SAAQ,YAAY;IACjD,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,gBAAgB,CAAe;IACvC,OAAO,CAAC,GAAG,CAAe;IAC1B,OAAO,CAAC,WAAW,CAAmB;IACtC,OAAO,CAAC,kBAAkB,CAA6B;gBAE3C,MAAM,GAAE,uBAA4B;IAahD,KAAK,IAAI,IAAI;IAkDb,IAAI,IAAI,IAAI;IAOZ;qFACiF;IACjF,OAAO,CAAC,gBAAgB;IAiBxB,OAAO,CAAC,iBAAiB;IAezB;;;;;;;;;;;;;;OAcG;IACH,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAanE,oDAAoD;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,cAAc;CAkB3C"}
|
|
@@ -6,36 +6,98 @@
|
|
|
6
6
|
* We detect this drift and fire a callback.
|
|
7
7
|
*
|
|
8
8
|
* Ported from Dawn's infrastructure — battle-tested in production.
|
|
9
|
+
*
|
|
10
|
+
* ## CPU-starvation guard (2026-05-28)
|
|
11
|
+
*
|
|
12
|
+
* Timer drift has TWO causes, not one:
|
|
13
|
+
* 1. Real sleep — the OS suspends the process; the wall clock jumps forward.
|
|
14
|
+
* 2. CPU starvation — the machine is so oversubscribed (load >> cores) that the
|
|
15
|
+
* event loop can't service the `checkIntervalMs` timer on time. The wall
|
|
16
|
+
* clock advances normally, but the callback fires seconds late.
|
|
17
|
+
*
|
|
18
|
+
* The original detector could not tell these apart, so on a heavily-loaded box
|
|
19
|
+
* (e.g. many concurrent agent sessions) it fired hundreds of false "wake" events
|
|
20
|
+
* — each triggering expensive wake-recovery (tunnel restart, re-registration,
|
|
21
|
+
* failure-counter resets) that piled MORE load on, a self-reinforcing storm.
|
|
22
|
+
*
|
|
23
|
+
* The guard distinguishes the two:
|
|
24
|
+
* - A **long** drift (>= `longSleepFloorSeconds`) is unambiguously real sleep —
|
|
25
|
+
* a live machine never starves its own event loop for minutes (a watchdog
|
|
26
|
+
* would declare it dead first). Always emitted, regardless of load.
|
|
27
|
+
* - A **short** drift under high system load (`loadavg[0] / cpuCount >
|
|
28
|
+
* maxLoadRatio`) is treated as CPU starvation and SUPPRESSED — no `wake`.
|
|
29
|
+
* - A short drift under normal load is a brief real sleep — emitted.
|
|
30
|
+
* On top of classification, a `minWakeIntervalMs` cooldown caps the emit rate so
|
|
31
|
+
* even a misclassified burst can't trigger a recovery storm.
|
|
32
|
+
*
|
|
33
|
+
* Suppressed events are NOT added to `wakeHistory`, so `getCumulativeSleepMsBetween`
|
|
34
|
+
* (the wake-reaper's sleep-credit source) only ever counts genuine sleep.
|
|
9
35
|
*/
|
|
10
36
|
import { EventEmitter } from 'node:events';
|
|
37
|
+
import os from 'node:os';
|
|
11
38
|
export class SleepWakeDetector extends EventEmitter {
|
|
12
39
|
interval = null;
|
|
13
|
-
lastTick
|
|
40
|
+
lastTick;
|
|
41
|
+
lastEmittedWakeAtMs = null;
|
|
14
42
|
checkIntervalMs;
|
|
15
43
|
driftThresholdMs;
|
|
44
|
+
maxLoadRatio;
|
|
45
|
+
longSleepFloorSeconds;
|
|
46
|
+
minWakeIntervalMs;
|
|
47
|
+
loadAvgProvider;
|
|
48
|
+
cpuCountProvider;
|
|
49
|
+
now;
|
|
16
50
|
wakeHistory = [];
|
|
51
|
+
suppressionHistory = [];
|
|
17
52
|
constructor(config = {}) {
|
|
18
53
|
super();
|
|
19
54
|
this.checkIntervalMs = config.checkIntervalMs ?? 2000;
|
|
20
55
|
this.driftThresholdMs = config.driftThresholdMs ?? 10000;
|
|
56
|
+
this.maxLoadRatio = config.maxLoadRatio ?? 1.5;
|
|
57
|
+
this.longSleepFloorSeconds = config.longSleepFloorSeconds ?? 300;
|
|
58
|
+
this.minWakeIntervalMs = config.minWakeIntervalMs ?? 60000;
|
|
59
|
+
this.loadAvgProvider = config.loadAvgProvider ?? (() => os.loadavg());
|
|
60
|
+
this.cpuCountProvider = config.cpuCountProvider ?? (() => os.cpus().length);
|
|
61
|
+
this.now = config.nowProvider ?? (() => Date.now());
|
|
62
|
+
this.lastTick = this.now();
|
|
21
63
|
}
|
|
22
64
|
start() {
|
|
23
65
|
if (this.interval)
|
|
24
66
|
return;
|
|
25
|
-
this.lastTick =
|
|
67
|
+
this.lastTick = this.now();
|
|
26
68
|
this.interval = setInterval(() => {
|
|
27
|
-
const now =
|
|
69
|
+
const now = this.now();
|
|
28
70
|
const elapsed = now - this.lastTick;
|
|
29
71
|
this.lastTick = now;
|
|
30
|
-
if (elapsed
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.
|
|
72
|
+
if (elapsed <= this.driftThresholdMs)
|
|
73
|
+
return;
|
|
74
|
+
const sleepDuration = Math.round((elapsed - this.checkIntervalMs) / 1000);
|
|
75
|
+
const isLongSleep = sleepDuration >= this.longSleepFloorSeconds;
|
|
76
|
+
const loadRatio = this.currentLoadRatio();
|
|
77
|
+
// Short drift under heavy CPU load = event-loop starvation, not sleep.
|
|
78
|
+
if (!isLongSleep && loadRatio > this.maxLoadRatio) {
|
|
79
|
+
this.recordSuppression('cpu-starvation', sleepDuration, loadRatio, now);
|
|
80
|
+
console.warn(`[SleepWakeDetector] Drift ~${sleepDuration}s under load ratio ${loadRatio.toFixed(2)} ` +
|
|
81
|
+
`(> ${this.maxLoadRatio}) — treating as CPU starvation, suppressing wake`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Rate-limit emitted wakes so even a misclassified burst can't storm
|
|
85
|
+
// recovery. Long sleeps bypass the cooldown — recovery there is essential.
|
|
86
|
+
if (!isLongSleep &&
|
|
87
|
+
this.lastEmittedWakeAtMs !== null &&
|
|
88
|
+
now - this.lastEmittedWakeAtMs < this.minWakeIntervalMs) {
|
|
89
|
+
this.recordSuppression('cooldown', sleepDuration, loadRatio, now);
|
|
90
|
+
console.warn(`[SleepWakeDetector] Wake within cooldown ` +
|
|
91
|
+
`(${now - this.lastEmittedWakeAtMs}ms < ${this.minWakeIntervalMs}ms) — suppressing duplicate recovery`);
|
|
92
|
+
return;
|
|
38
93
|
}
|
|
94
|
+
console.log(`[SleepWakeDetector] Wake detected after ~${sleepDuration}s sleep`);
|
|
95
|
+
const event = { sleepDurationSeconds: sleepDuration, timestamp: new Date(now).toISOString() };
|
|
96
|
+
this.wakeHistory.push(event);
|
|
97
|
+
if (this.wakeHistory.length > 100)
|
|
98
|
+
this.wakeHistory.shift();
|
|
99
|
+
this.lastEmittedWakeAtMs = now;
|
|
100
|
+
this.emit('wake', event);
|
|
39
101
|
}, this.checkIntervalMs);
|
|
40
102
|
this.interval.unref(); // Don't prevent process exit in CLI contexts
|
|
41
103
|
}
|
|
@@ -45,6 +107,37 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
45
107
|
this.interval = null;
|
|
46
108
|
}
|
|
47
109
|
}
|
|
110
|
+
/** Current `loadavg[0] / cpuCount` ratio. Returns 0 when load is unavailable
|
|
111
|
+
* (e.g. Windows reports [0,0,0]), which disables the starvation guard there. */
|
|
112
|
+
currentLoadRatio() {
|
|
113
|
+
let cpuCount = 1;
|
|
114
|
+
try {
|
|
115
|
+
cpuCount = Math.max(1, this.cpuCountProvider());
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
cpuCount = 1;
|
|
119
|
+
}
|
|
120
|
+
let load1 = 0;
|
|
121
|
+
try {
|
|
122
|
+
load1 = this.loadAvgProvider()[0] ?? 0;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
load1 = 0;
|
|
126
|
+
}
|
|
127
|
+
if (!Number.isFinite(load1) || load1 <= 0)
|
|
128
|
+
return 0;
|
|
129
|
+
return load1 / cpuCount;
|
|
130
|
+
}
|
|
131
|
+
recordSuppression(reason, driftSeconds, loadRatio, nowMs) {
|
|
132
|
+
this.suppressionHistory.push({
|
|
133
|
+
reason,
|
|
134
|
+
driftSeconds,
|
|
135
|
+
loadRatio: Math.round(loadRatio * 100) / 100,
|
|
136
|
+
timestamp: new Date(nowMs).toISOString(),
|
|
137
|
+
});
|
|
138
|
+
if (this.suppressionHistory.length > 100)
|
|
139
|
+
this.suppressionHistory.shift();
|
|
140
|
+
}
|
|
48
141
|
/**
|
|
49
142
|
* Cumulative wall-time-asleep during the half-open window [startMs, endMs).
|
|
50
143
|
* Used by the wake-reaper (UNIFIED-SESSION-LIFECYCLE §P0 #9 / SE-8) to subtract
|
|
@@ -56,6 +149,9 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
56
149
|
* [wakeTimestamp − sleepDurationSeconds, wakeTimestamp].
|
|
57
150
|
* Returns the sum of overlap with the query window in milliseconds. Returns 0
|
|
58
151
|
* when the history is empty or no event overlaps.
|
|
152
|
+
*
|
|
153
|
+
* Only EMITTED wakes are in `wakeHistory`; suppressed CPU-starvation drifts are
|
|
154
|
+
* deliberately excluded so starvation is never credited as real sleep.
|
|
59
155
|
*/
|
|
60
156
|
getCumulativeSleepMsBetween(startMs, endMs) {
|
|
61
157
|
if (endMs <= startMs)
|
|
@@ -75,10 +171,20 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
75
171
|
getStats(sinceMs) {
|
|
76
172
|
const since = sinceMs ?? 0;
|
|
77
173
|
const relevant = this.wakeHistory.filter(e => new Date(e.timestamp).getTime() >= since);
|
|
174
|
+
const suppressed = this.suppressionHistory.filter(e => new Date(e.timestamp).getTime() >= since);
|
|
175
|
+
const suppressedByReason = {
|
|
176
|
+
'cpu-starvation': 0,
|
|
177
|
+
cooldown: 0,
|
|
178
|
+
};
|
|
179
|
+
for (const e of suppressed)
|
|
180
|
+
suppressedByReason[e.reason]++;
|
|
78
181
|
return {
|
|
79
182
|
wakeCount: relevant.length,
|
|
80
183
|
totalSleepSeconds: relevant.reduce((sum, e) => sum + e.sleepDurationSeconds, 0),
|
|
81
184
|
longestSleepSeconds: relevant.length > 0 ? Math.max(...relevant.map(e => e.sleepDurationSeconds)) : 0,
|
|
185
|
+
suppressedCount: suppressed.length,
|
|
186
|
+
suppressedByReason,
|
|
187
|
+
lastSuppressedAt: suppressed.length > 0 ? suppressed[suppressed.length - 1].timestamp : null,
|
|
82
188
|
};
|
|
83
189
|
}
|
|
84
190
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SleepWakeDetector.js","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"SleepWakeDetector.js","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UsherActedCorrelator — the precision-numerator wiring for the Usher (rung 4 of
|
|
3
|
+
* continuous-working-awareness).
|
|
4
|
+
*
|
|
5
|
+
* The Usher FIRES re-surface signals (UsherSignalStore.recordSignal). Until this
|
|
6
|
+
* module, nothing ever called markActed — so `precision = acted/fired` was pinned
|
|
7
|
+
* at 0 and the rung-5 (mid-task injection) gate could never be satisfied by data.
|
|
8
|
+
* This closes that loop via TWO correlation paths, both reducing to "does some
|
|
9
|
+
* probe text COVER a fired signal's contextText?":
|
|
10
|
+
*
|
|
11
|
+
* (a) auto-use — the agent's next reply on the topic uses the re-surfaced
|
|
12
|
+
* context. creditUsherOnOutbound(). The nudge was useful: the
|
|
13
|
+
* agent acted on it.
|
|
14
|
+
* (b) miss-map — the user later had to CORRECT the agent (a HumanAsDetector
|
|
15
|
+
* signal) on that same context. creditUsherOnMiss(). The nudge
|
|
16
|
+
* was a genuine catch the agent ignored — still a true positive
|
|
17
|
+
* for the Usher's precision.
|
|
18
|
+
*
|
|
19
|
+
* Matching is deliberately PRECISION-OVER-RECALL: precision gates whether the
|
|
20
|
+
* Usher earns the right to interrupt (rung 5), so a falsely-HIGH precision is the
|
|
21
|
+
* dangerous direction. We require a meaningful salient-term overlap (≥ MIN_SHARED
|
|
22
|
+
* shared content words AND ≥ MIN_COVERAGE of the context's terms) and only credit
|
|
23
|
+
* recently-fired signals. Under-crediting a fast reply is acceptable; inflating
|
|
24
|
+
* the gate is not.
|
|
25
|
+
*
|
|
26
|
+
* All exported integration fns are best-effort and NEVER throw into the
|
|
27
|
+
* message/delivery path — mirroring the Usher's own degrade-safety invariants.
|
|
28
|
+
*/
|
|
29
|
+
import type { UsherSignal, UsherSignalStore } from './UsherSignalStore.js';
|
|
30
|
+
/** Minimal inbound-entry shape the miss path reads (matches InboundMessageEntry). */
|
|
31
|
+
export interface MissEntry {
|
|
32
|
+
topicId?: number | null;
|
|
33
|
+
text?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare const MIN_SHARED = 2;
|
|
36
|
+
export declare const MIN_COVERAGE = 0.5;
|
|
37
|
+
/** Window in which the agent's reply still counts as "using" a just-fired nudge. */
|
|
38
|
+
export declare const USE_WINDOW_MS: number;
|
|
39
|
+
/** Window in which a later user correction still credits a prior nudge. */
|
|
40
|
+
export declare const MISS_WINDOW_MS: number;
|
|
41
|
+
/**
|
|
42
|
+
* Salient content tokens of a text: lowercased, alphanumeric-split, ≥3 chars,
|
|
43
|
+
* stopwords removed. Pure + deterministic.
|
|
44
|
+
*/
|
|
45
|
+
export declare function salientTerms(text: string): Set<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Does `probeText` cover enough of `contextText`'s salient terms to count as a
|
|
48
|
+
* genuine reference to it? Requires ≥ minShared shared terms AND ≥ minCoverage
|
|
49
|
+
* fraction of the context's terms (so single-coincidental-word matches fail, and
|
|
50
|
+
* a long context needs proportionally more overlap).
|
|
51
|
+
*/
|
|
52
|
+
export declare function contextCoveredBy(contextText: string, probeText: string, opts?: {
|
|
53
|
+
minShared?: number;
|
|
54
|
+
minCoverage?: number;
|
|
55
|
+
}): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Pure: among NOT-yet-acted signals, which ones does `probeText` cover? Returns
|
|
58
|
+
* their ids. `maxAgeMs`+`nowMs` optionally drop signals older than the window.
|
|
59
|
+
*/
|
|
60
|
+
export declare function findCoveredSignalIds(signals: UsherSignal[], probeText: string, opts?: {
|
|
61
|
+
minShared?: number;
|
|
62
|
+
minCoverage?: number;
|
|
63
|
+
maxAgeMs?: number;
|
|
64
|
+
nowMs?: number;
|
|
65
|
+
}): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Core integration: correlate `probeText` against a topic's fired signals and
|
|
68
|
+
* mark the matches acted (stamped with `via`). Best-effort, never throws.
|
|
69
|
+
* Returns the ids actually marked.
|
|
70
|
+
*/
|
|
71
|
+
export declare function markActedByCoverage(store: UsherSignalStore, topicId: number, probeText: string, via: 'use' | 'miss', opts?: {
|
|
72
|
+
now?: () => number;
|
|
73
|
+
maxAgeMs?: number;
|
|
74
|
+
minShared?: number;
|
|
75
|
+
minCoverage?: number;
|
|
76
|
+
onMarked?: (id: string) => void;
|
|
77
|
+
}): string[];
|
|
78
|
+
/**
|
|
79
|
+
* Path (a): the agent's reply just went out on `topicId`. Credit any recent
|
|
80
|
+
* faded-context nudge the reply actually used. Wire on the outbound-reply path.
|
|
81
|
+
*/
|
|
82
|
+
export declare function creditUsherOnOutbound(store: UsherSignalStore | null | undefined, topicId: number, replyText: string, opts?: {
|
|
83
|
+
now?: () => number;
|
|
84
|
+
maxAgeMs?: number;
|
|
85
|
+
}): string[];
|
|
86
|
+
/**
|
|
87
|
+
* Path (b): the user just had to correct the agent (`missSignal` non-null). If a
|
|
88
|
+
* recent nudge on this topic covers what they're correcting, that nudge was a
|
|
89
|
+
* real catch the agent ignored → credit it. Wire on the inbound human-detector
|
|
90
|
+
* seam. No-op when there was no miss, no topic, or no text.
|
|
91
|
+
*/
|
|
92
|
+
export declare function creditUsherOnMiss(store: UsherSignalStore | null | undefined, missSignal: unknown, entry: MissEntry, opts?: {
|
|
93
|
+
now?: () => number;
|
|
94
|
+
maxAgeMs?: number;
|
|
95
|
+
}): string[];
|
|
96
|
+
//# sourceMappingURL=UsherActedCorrelator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UsherActedCorrelator.d.ts","sourceRoot":"","sources":["../../src/core/UsherActedCorrelator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE3E,qFAAqF;AACrF,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAGD,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,eAAO,MAAM,YAAY,MAAM,CAAC;AAChC,oFAAoF;AACpF,eAAO,MAAM,aAAa,QAAqB,CAAC;AAChD,2EAA2E;AAC3E,eAAO,MAAM,cAAc,QAAsB,CAAC;AAgBlD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAStD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAClD,OAAO,CAST;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACrF,MAAM,EAAE,CAaV;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,KAAK,GAAG,MAAM,EACnB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAC1H,MAAM,EAAE,CAyBV;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC1C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,MAAM,EAAE,CAMV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC1C,UAAU,EAAE,OAAO,EACnB,KAAK,EAAE,SAAS,EAChB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,MAAM,EAAE,CASV"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UsherActedCorrelator — the precision-numerator wiring for the Usher (rung 4 of
|
|
3
|
+
* continuous-working-awareness).
|
|
4
|
+
*
|
|
5
|
+
* The Usher FIRES re-surface signals (UsherSignalStore.recordSignal). Until this
|
|
6
|
+
* module, nothing ever called markActed — so `precision = acted/fired` was pinned
|
|
7
|
+
* at 0 and the rung-5 (mid-task injection) gate could never be satisfied by data.
|
|
8
|
+
* This closes that loop via TWO correlation paths, both reducing to "does some
|
|
9
|
+
* probe text COVER a fired signal's contextText?":
|
|
10
|
+
*
|
|
11
|
+
* (a) auto-use — the agent's next reply on the topic uses the re-surfaced
|
|
12
|
+
* context. creditUsherOnOutbound(). The nudge was useful: the
|
|
13
|
+
* agent acted on it.
|
|
14
|
+
* (b) miss-map — the user later had to CORRECT the agent (a HumanAsDetector
|
|
15
|
+
* signal) on that same context. creditUsherOnMiss(). The nudge
|
|
16
|
+
* was a genuine catch the agent ignored — still a true positive
|
|
17
|
+
* for the Usher's precision.
|
|
18
|
+
*
|
|
19
|
+
* Matching is deliberately PRECISION-OVER-RECALL: precision gates whether the
|
|
20
|
+
* Usher earns the right to interrupt (rung 5), so a falsely-HIGH precision is the
|
|
21
|
+
* dangerous direction. We require a meaningful salient-term overlap (≥ MIN_SHARED
|
|
22
|
+
* shared content words AND ≥ MIN_COVERAGE of the context's terms) and only credit
|
|
23
|
+
* recently-fired signals. Under-crediting a fast reply is acceptable; inflating
|
|
24
|
+
* the gate is not.
|
|
25
|
+
*
|
|
26
|
+
* All exported integration fns are best-effort and NEVER throw into the
|
|
27
|
+
* message/delivery path — mirroring the Usher's own degrade-safety invariants.
|
|
28
|
+
*/
|
|
29
|
+
// ── Matching knobs (precision-over-recall) ───────────────────────────────────
|
|
30
|
+
export const MIN_SHARED = 2; // a single coincidental word can't credit a signal
|
|
31
|
+
export const MIN_COVERAGE = 0.5; // must cover at least half the context's salient terms
|
|
32
|
+
/** Window in which the agent's reply still counts as "using" a just-fired nudge. */
|
|
33
|
+
export const USE_WINDOW_MS = 6 * 60 * 60 * 1000; // 6h
|
|
34
|
+
/** Window in which a later user correction still credits a prior nudge. */
|
|
35
|
+
export const MISS_WINDOW_MS = 24 * 60 * 60 * 1000; // 24h
|
|
36
|
+
// English stopwords + a few instar-conversation fillers. Kept small and stable:
|
|
37
|
+
// the goal is to drop function words so overlap reflects real subject matter.
|
|
38
|
+
const STOPWORDS = new Set([
|
|
39
|
+
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'your', 'with', 'that', 'this',
|
|
40
|
+
'have', 'has', 'had', 'was', 'were', 'will', 'would', 'should', 'could', 'can',
|
|
41
|
+
'its', 'it', 'is', 'be', 'been', 'being', 'from', 'into', 'over', 'under', 'about',
|
|
42
|
+
'they', 'them', 'their', 'there', 'here', 'what', 'when', 'where', 'which', 'who',
|
|
43
|
+
'how', 'why', 'all', 'any', 'some', 'one', 'two', 'now', 'then', 'than', 'just',
|
|
44
|
+
'out', 'off', 'too', 'very', 'our', 'his', 'her', 'him', 'she', 'his', 'we',
|
|
45
|
+
'get', 'got', 'let', 'see', 'use', 'used', 'make', 'made', 'want', 'need',
|
|
46
|
+
'yes', 'yeah', 'okay', 'sure', 'thing', 'things', 'stuff', 'like', 'also',
|
|
47
|
+
'going', 'gonna', 'really', 'much', 'more', 'most', 'still', 'back', 'done',
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Salient content tokens of a text: lowercased, alphanumeric-split, ≥3 chars,
|
|
51
|
+
* stopwords removed. Pure + deterministic.
|
|
52
|
+
*/
|
|
53
|
+
export function salientTerms(text) {
|
|
54
|
+
const out = new Set();
|
|
55
|
+
if (!text || typeof text !== 'string')
|
|
56
|
+
return out;
|
|
57
|
+
for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
58
|
+
if (raw.length < 3)
|
|
59
|
+
continue;
|
|
60
|
+
if (STOPWORDS.has(raw))
|
|
61
|
+
continue;
|
|
62
|
+
out.add(raw);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Does `probeText` cover enough of `contextText`'s salient terms to count as a
|
|
68
|
+
* genuine reference to it? Requires ≥ minShared shared terms AND ≥ minCoverage
|
|
69
|
+
* fraction of the context's terms (so single-coincidental-word matches fail, and
|
|
70
|
+
* a long context needs proportionally more overlap).
|
|
71
|
+
*/
|
|
72
|
+
export function contextCoveredBy(contextText, probeText, opts) {
|
|
73
|
+
const ctx = salientTerms(contextText);
|
|
74
|
+
if (ctx.size === 0)
|
|
75
|
+
return false;
|
|
76
|
+
const probe = salientTerms(probeText);
|
|
77
|
+
if (probe.size === 0)
|
|
78
|
+
return false;
|
|
79
|
+
let shared = 0;
|
|
80
|
+
for (const t of ctx)
|
|
81
|
+
if (probe.has(t))
|
|
82
|
+
shared++;
|
|
83
|
+
const coverage = shared / ctx.size;
|
|
84
|
+
return shared >= (opts?.minShared ?? MIN_SHARED) && coverage >= (opts?.minCoverage ?? MIN_COVERAGE);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pure: among NOT-yet-acted signals, which ones does `probeText` cover? Returns
|
|
88
|
+
* their ids. `maxAgeMs`+`nowMs` optionally drop signals older than the window.
|
|
89
|
+
*/
|
|
90
|
+
export function findCoveredSignalIds(signals, probeText, opts) {
|
|
91
|
+
const ids = [];
|
|
92
|
+
const maxAgeMs = opts?.maxAgeMs;
|
|
93
|
+
const nowMs = opts?.nowMs ?? Date.now();
|
|
94
|
+
for (const s of signals) {
|
|
95
|
+
if (!s || s.acted)
|
|
96
|
+
continue;
|
|
97
|
+
if (maxAgeMs !== undefined) {
|
|
98
|
+
const firedMs = Date.parse(s.at);
|
|
99
|
+
if (!Number.isNaN(firedMs) && nowMs - firedMs > maxAgeMs)
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (contextCoveredBy(s.contextText, probeText, opts))
|
|
103
|
+
ids.push(s.id);
|
|
104
|
+
}
|
|
105
|
+
return ids;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Core integration: correlate `probeText` against a topic's fired signals and
|
|
109
|
+
* mark the matches acted (stamped with `via`). Best-effort, never throws.
|
|
110
|
+
* Returns the ids actually marked.
|
|
111
|
+
*/
|
|
112
|
+
export function markActedByCoverage(store, topicId, probeText, via, opts) {
|
|
113
|
+
try {
|
|
114
|
+
if (!probeText || typeof probeText !== 'string')
|
|
115
|
+
return [];
|
|
116
|
+
if (typeof topicId !== 'number' || !Number.isFinite(topicId))
|
|
117
|
+
return [];
|
|
118
|
+
const nowMs = (opts?.now ?? (() => Date.now()))();
|
|
119
|
+
const signals = store.getSignals(topicId, 50);
|
|
120
|
+
const ids = findCoveredSignalIds(signals, probeText, {
|
|
121
|
+
minShared: opts?.minShared,
|
|
122
|
+
minCoverage: opts?.minCoverage,
|
|
123
|
+
maxAgeMs: opts?.maxAgeMs,
|
|
124
|
+
nowMs,
|
|
125
|
+
});
|
|
126
|
+
if (ids.length === 0)
|
|
127
|
+
return [];
|
|
128
|
+
const at = new Date(nowMs).toISOString();
|
|
129
|
+
const marked = [];
|
|
130
|
+
for (const id of ids) {
|
|
131
|
+
if (store.markActed(topicId, id, { via, at })) {
|
|
132
|
+
marked.push(id);
|
|
133
|
+
try {
|
|
134
|
+
opts?.onMarked?.(id);
|
|
135
|
+
}
|
|
136
|
+
catch { /* observability hook must not break */ }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return marked;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Path (a): the agent's reply just went out on `topicId`. Credit any recent
|
|
147
|
+
* faded-context nudge the reply actually used. Wire on the outbound-reply path.
|
|
148
|
+
*/
|
|
149
|
+
export function creditUsherOnOutbound(store, topicId, replyText, opts) {
|
|
150
|
+
if (!store)
|
|
151
|
+
return [];
|
|
152
|
+
return markActedByCoverage(store, topicId, replyText, 'use', {
|
|
153
|
+
now: opts?.now,
|
|
154
|
+
maxAgeMs: opts?.maxAgeMs ?? USE_WINDOW_MS,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Path (b): the user just had to correct the agent (`missSignal` non-null). If a
|
|
159
|
+
* recent nudge on this topic covers what they're correcting, that nudge was a
|
|
160
|
+
* real catch the agent ignored → credit it. Wire on the inbound human-detector
|
|
161
|
+
* seam. No-op when there was no miss, no topic, or no text.
|
|
162
|
+
*/
|
|
163
|
+
export function creditUsherOnMiss(store, missSignal, entry, opts) {
|
|
164
|
+
if (!store || !missSignal)
|
|
165
|
+
return [];
|
|
166
|
+
const topicId = entry?.topicId;
|
|
167
|
+
const text = entry?.text;
|
|
168
|
+
if (typeof topicId !== 'number' || !text)
|
|
169
|
+
return [];
|
|
170
|
+
return markActedByCoverage(store, topicId, text, 'miss', {
|
|
171
|
+
now: opts?.now,
|
|
172
|
+
maxAgeMs: opts?.maxAgeMs ?? MISS_WINDOW_MS,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=UsherActedCorrelator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UsherActedCorrelator.js","sourceRoot":"","sources":["../../src/core/UsherActedCorrelator.ts"],"names":[],"mappings":"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"}
|